diff --git a/.envrc b/.envrc index ccf83c95b43..fbb67a797ad 100644 --- a/.envrc +++ b/.envrc @@ -5,6 +5,15 @@ # or any of the `default.nix` files change. We do this by adding all these files # to the nix store and using the store paths as a cache key. +# FUTUREWORK: The speedhack saves only a couple of Seconds when cd'ing into the +# repo's directory. The price is some state we have to manage manually (e.g. +# `rm .direnv`) and a non-intuitive usage of flakes. +# +# The hack could be replaced by the line +# `use flake .\#` +# +# The env variable exports could then be added to the flake's devShell. + nix_files=$(find . -name '*.nix' | grep -v '^./dist-newstyle') for nix_file in $nix_files; do watch_file "$nix_file" diff --git a/.gitignore b/.gitignore index c2a3859bbdd..041ace3a270 100644 --- a/.gitignore +++ b/.gitignore @@ -106,5 +106,8 @@ logs-integration # BOM file - https://github.com/wireapp/tom-bombadil sbom.json +# Temporary files +tmp/ + # HLS config for haskell-tools plugin (Neovim) hls.json diff --git a/AGENTS.md b/AGENTS.md index fc39cbe769e..f180872b2d3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -105,3 +105,78 @@ Build commands (e.g. `make c`, `cabal build` or `cabal test`) should always end with this command line filter: `| grep -vE 'Compiling|Linking|Preprocessing|Configuring|Building'`. The filter ensures that only relevant output is displayed. + +# Security Guidelines + +For all code generated in this chat, adhere to the following guidelines: + +### 1. Be Explicit About Security Requirements +- Always request and enforce secure coding practices directly. +- Example prompts to follow: + - “Generate a Python function to query a database using parameterized queries and no string concatenation.” + - “Write input validation code for usernames using a whitelist approach.” +- Do **not** accept vague prompts such as: + - “Write code to query a database.” + - “Generate authentication logic.” + +### 2. Prevent Sensitive Data Leaks +- Never generate production secrets, API keys, credentials, personal data, or customer-identifying information. +- Always use placeholders (e.g., `YOUR_API_KEY_HERE`) or synthetic/anonymized data. + +### 3. Limit Scope & Keep It Modular +- Focus on narrow, well-defined tasks. +- Example: “Generate a secure password hashing function using Argon2id with parameters selected according to the OWASP Password Storage Cheat Sheet.” +- Reject overly broad requests like “Write a full secure authentication system.” +- Propose security requirements before generating code. +- Discuss alternatives, evaluate them, and provide a step-by-step implementation plan. + +### 4. Specify Safe Practices & Standards +- Always reference frameworks, versions, and security guidelines. +- Example: “Use Flask 3.0 and follow OWASP secure coding practices.” +- Example: “Use your web framework’s built-in auto-escaping/output encoding to prevent XSS.” +- Example: “Sanitize user-provided rich HTML content on the client using DOMPurify before rendering.” +### 5. Control Library Choices +- Only use secure, widely adopted, actively maintained dependencies. +- Ensure libraries have no known high-severity CVEs. +- Example: “Suggest Node.js packages updated in the last 6 months.” + +### 6. Request Validation & Error Handling +- Always include strict server-side validation. +- Error messages must be generic and never disclose sensitive details. +- Example: “Generate a file upload handler with size limits, MIME type checks, and safe error messages.” + +### 7. Avoid Dangerous Features by Default +- Exclude risky features unless explicitly requested. +- Example: + - Do not use `eval`. + - Do not disable SSL/TLS verification. + +### 8. Request Tests for Security +- Always generate tests verifying security protections. +- Integrate tests into CI/CD pipelines. +- Examples: + - “Write unit tests confirming rejection of invalid input and prevention of XSS.” + - “Generate a pytest suite covering edge cases, including malicious inputs.” + +### 9. Ask for Explanations +- Always justify security decisions in generated code. +- Example: “Explain how this code prevents SQL injection.” +- Example: “List all security measures in your code.” + +### 10. Avoid Blind Trust in Input Handling +- Do not assume inputs are safe. +- Explicitly include strict validation. +- Always use parameterized queries. + +### 11. Constrain the Role +- Only fulfill narrowly scoped coding tasks. +- Example: “Generate input validation for email addresses only.” +- Reject broad, open-ended prompts like: + - “Write a secure web app.” + +### 12. Follow Up Iteratively +- Treat the first response as a draft. +- Accept refinements and improvements from the user. +- Example refinements: + - “Improve error handling to avoid information disclosure.” + - “Replace insecure library X with a more secure alternative.” diff --git a/CHANGELOG.md b/CHANGELOG.md index 10a1154a507..2ca4a7fb4f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,126 @@ +# [2026-03-24] (Chart Release 5.29.0) + +## Release notes + + +* Helm chart refactoring: several core services were migrated from wire-server subcharts into the umbrella chart templates (`charts/wire-server/templates`). + + Moved as core services: + - `background-worker` + - `brig` + - `cannon` + - `cargohold` + - `galley` + - `gundeck` + - `proxy` + - `spar` + + As a result, dependency tags for moved services are obsolete for the current wire-server chart, because these services are no longer resolved through `requirements.yaml` dependencies. In particular, `tags.brig`, `tags.galley`, `tags.cannon`, `tags.cargohold`, `tags.gundeck`, `tags.proxy`, and `tags.spar` are no longer needed for wire-server deployments. + + Operator note: during upgrade, rendered manifests will show metadata/source changes (for example chart labels and template source paths). This is expected from the inlining refactor and may trigger a one-time rollout due to checksum annotation changes. + + Compatibility note: for standard wire-server deployments this is not expected to be breaking, because the moved core services were not toggled off via tags in default/in-repo environments. However, this is a breaking change for custom deployments that previously disabled any of these services via wire-server dependency tags (`tags.: false`), because those tags are now obsolete after inlining. (#5085) + +* Rate-limit status codes in `nginz` and cannon's embedded `nginz` are now configurable via Helm values. + + Compatibility note: the default remains `420`, so this does not change behavior for existing deployments and requires no direct operator action. (#5124) + +* Remove the old `metallb` wrapper chart. This hasn't been published or updated + for quite some time. Even the Docker images weren't available anymore. (#5111) + + +## API changes + + +* Require admin password for refreshing app cookies (`POST /teams/:tid/apps/:uid/cookies`). (#5129) + +* Add `"app"` attribute to `GET /list-users`, `GET /users/:dom/:uid`; make `GET /teams/:tid/apps`, `GET /teams/:tid/apps/:uid` return same schema as `GET /list-users`. (#5070) + +* `GET /teams/:tid/search` response contains user types now (app or regular). (#5074) + +* - remove pict attribute from GetApp + - remove metadata attribute from GetApp + - inline GetApp fields into NewApp + - make CreatedApp.user contain UserProfile (which includes app info) + - rename GetApp to AppInfo + - remove AppInfo.{name,assets,accentId} (redundant user data) (#5115) + +* Create new API version V16 and finalize API version V15. (#5121) + + +## Features + + +* Add meetings listings endpoint `/meetings/list`. (#5109) + +* Add Wire Meetings add invitation endpoint `POST /meetings/:domain/:id/invitations` (#5132) + +* Add Wire Meetings delete invitation endpoint `POST /meetings/:domain/:id/invitations/delete` (#5136) + + +## Bug fixes and other updates + + +* Claiming key packages for a deleted user now returns a client error instead of a server error (#5113) + +* backoffice/stern: fix Swagger UI for comma-separated list query parameters (#5108) + +* Streamlined and fixed team feature config `validateSAMLemails` (#5114) + +* When the admin creates a new app cookie, all previous ones must be revoked. (#5149) + +* charts/elasticsearch-index: Allow configuring postgresql (#5092) + +* charts/wire-server: Fix nil pointer errors in merged subchart templates when optional values (brig.turn, rabbitmq TLS, cassandraBrig/cassandraGalley) are not provided (#5112) + +* Improve error message when failing to parse group ID (#5089) + + +## Documentation + + +* Updated docs for the team feature `validateSAMLemails` (#5118) + + +## Internal changes + + +* The status code for rate limit responses from nginz and cannon is now configurable and set to 420 per default (#5124) + +* Add curl to integration test failure reports. (#5048) + +* Add `UserType` fields in various data types. (#5074) + +* Progressively move away from singletons to type class to allow progressive migration to `wire-subsystems` of Galley's actions. + Drop `Galley.Intra.Util`,`Galley.Effects`, `Galley.API.MLS.Commit`, and `Galley.API.Push`. + Break dependencies to `Opts`/`Env`. + Split `ConversationSubsystem.Interpreter` `Galley.API.Federation` (#5075, #5081, #5086, #5087, #5098, #5101, #5102, #5103, #5104, #5110, #5145, #5148) + +* Logging Wire-Client, Wire-Client-Version and Wire-Config-Hash headers in nginz (#5123) + +* Refactor scripts to alleviate SonarQube warnings (#5097) + +* Consumable notifications are now disabled (#5116) + +* The fields `code`, `label`, and `message` where added to the inconsistent group state error response of `POST /mls/commit-bundels` (#PR_NOT_FOUND) + +* Refactor Category: from ADT to Text. (#5120) + +* Moved TeamVisibilityStore operations into TeamStore (#5137) + +* Moved TeamNotificationStore to wire-subsystems (#5138) + +* Moved CustomBackendStore to wire-subsystems (#5135) + +* Moved TeamMemberStore, interpreter, and ListItems interpreters for Team to wire-subsystems (#5140) + +* `sbomqs` has been unused for years now. Thus, dropping it from our Nix env. (#5144) + +* Adjust the `default` Nix flake `devShell` such that `nix develop` is usable. (#5127) + +* Create and upload SBOMs for Helmfile, docker-compose and Helm charts. (#5122) + + # [2026-03-03] (Chart Release 5.28.0) ## Release notes diff --git a/Makefile b/Makefile index 13f60a50260..49407d77299 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,7 @@ KIND_CLUSTER_NAME := wire-server HELM_PARALLELISM ?= 1 # 1 for sequential tests; 6 for all-parallel tests PSQL_DB ?= backendA export PSQL_DB +DEPENDENCY_TRACK_PROJECT_NAME ?= wire-server package ?= all EXE_SCHEMA := ./dist/$(package)-schema @@ -76,6 +77,7 @@ 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" + -rm -rf tmp/sboms .PHONY: clean-hint clean-hint: @@ -122,7 +124,7 @@ endif .PHONY: ci-safe ci-safe: make c package=all - ./hack/bin/cabal-run-integration.sh integration + make ci-fast .PHONY: ci ci: @@ -347,9 +349,9 @@ postgres-schema-impl: .PHONY: cqlsh cqlsh: - $(eval CASSANDRA_CONTAINER := $(shell docker ps | grep '/cassandra:' | perl -ne '/^(\S+)\s/ && print $$1')) + $(eval CASSANDRA_CONTAINER := $(shell docker ps | grep 'cassandra' | perl -ne '/^(\S+)\s/ && print $$1')) @echo "make sure you have ./deploy/dockerephemeral/run.sh running in another window!" - docker exec -it $(CASSANDRA_CONTAINER) /usr/bin/cqlsh + docker exec -it $(CASSANDRA_CONTAINER) cqlsh .PHONY: psql psql: @@ -565,6 +567,11 @@ charts-serve-all: $(foreach chartName,$(CHARTS_RELEASE),chart-$(chartName)) .PHONY: charts-release charts-release: $(foreach chartName,$(CHARTS_RELEASE),release-chart-$(chartName)) +# Prepare .local/charts to be read by `helmfile` +.PHONY: .local/charts +.local/charts: charts-release + ./hack/bin/prepare-local-charts.sh $(CHARTS_RELEASE) + .PHONY: clean-charts clean-charts: rm -rf .local/charts @@ -674,6 +681,28 @@ kind-restart-%: .local/kind-kubeconfig helm-template-%: clean-charts charts-integration ./hack/bin/helm-template.sh $(*) +# Render the wire-server manifest from an explicit values file. +# Usage: +# make render-manifest VALUES_FILE=/tmp/values.yaml +# make render-manifest VALUES_FILE=/tmp/values.yaml OUTPUT_FILE=/tmp/rendered.yaml +# (you can get the live values e.g. like this: helm get values wire-server -n wire -a) +render-manifest: clean-charts charts-integration + ./hack/bin/render-manifest.sh "$(VALUES_FILE)" + +# Render wire-server from live values and compare it with the live manifest. +# Usage: +# helm get values wire-server -n wire -a > /tmp/staging/live-values.yaml +# helm get manifest wire-server -n wire > /tmp/staging/live-manifest.yaml +# make diff-live-manifest LIVE_VALUES_FILE=/tmp/staging/live-values.yaml LIVE_MANIFEST_FILE=/tmp/staging/live-manifest.yaml +diff-live-manifest: clean-charts charts-integration + OUTPUT_FILE="/tmp/wire-server.yaml" ./hack/bin/render-manifest.sh "$(LIVE_VALUES_FILE)"; \ + DIFF_OUTPUT_FILE="$(DIFF_OUTPUT_FILE)" ./hack/bin/diff-wire-server-manifests.sh "$(LIVE_MANIFEST_FILE)" /tmp/wire-server.yaml + +render-ci-manifest: clean-charts charts-integration + VALUES_FILE="$${VALUES_FILE:-$$(mktemp).yaml}"; \ + ./hack/bin/helm-render-ci-values.sh \ + ./hack/bin/render-manifest.sh "$$VALUES_FILE" + sbom.json: nix -Lv build '.#wireServer.bomDependencies' && \ nix run 'github:wireapp/tom-bombadil#create-sbom' -- --root-package-name "wire-server" @@ -687,6 +716,66 @@ upload-bombon: sbom.json --auto-create \ --bom-file ./sbom.json +# SBOM creation and uploading (Helm charts, Helmfile, docker-compose) +# +# For non-Nix environments (Kubernetes, docker-compose) and Helm charts we can +# use the usual tools and do not need tom-bombadil. +# +# There is a Nix `devShell` which provides an environment for these targets, `sbom`. +# E.g. to run the `sboms` target: +# `nix develop .\#sbom --command make sboms HELM_SEMVER=... DOCKER_TAG=...` +# +# Why don't we simply add this `nix develop` call to the Makefile targets? +# Targets should be independently executable and creating a Nix env in a Nix +# env doesn't play well. + +# Generate all SBOMs (Helm + Docker Compose + Helmfile) +.PHONY: sboms +sboms: sboms-helm sboms-docker-compose sboms-helmfile + +# Generate SBOMs for Helm charts +.PHONY: sboms-helm +sboms-helm: .local/charts + @if [ "$(HELM_SEMVER)" = "0.0.42" ]; then \ + echo "Environment variable HELM_SEMVER not set to non-default value. Re-run with HELM_SEMVER="; \ + exit 1; \ + fi + ./hack/bin/create-helm-sboms.sh tmp/sboms/helm $(HELM_SEMVER) + +# Generate SBOMs for Docker Compose +.PHONY: sboms-docker-compose +sboms-docker-compose: + ./hack/bin/create-docker-compose-sboms.sh tmp/sboms/docker-compose + +# Generate SBOMs for Helmfile +.PHONY: sboms-helmfile +sboms-helmfile: .local/charts + @if [ "$(HELM_SEMVER)" = "0.0.42" ]; then \ + echo "Environment variable HELM_SEMVER not set to non-default value. Re-run with HELM_SEMVER="; \ + exit 1; \ + fi + ./hack/bin/create-helmfile-sboms.sh tmp/sboms/helmfile $(HELM_SEMVER) + +# Validate all SBOM files using cyclonedx +.PHONY: validate-sboms +validate-sboms: + @echo "Validating SBOM files..." + @find tmp/sboms -name '*.json' -type f -not -path '*/.oci-cache/*' | while read sbom; do \ + echo "Validating: $$sbom"; \ + cyclonedx validate --input-file "$$sbom" --fail-on-errors; \ + done + @echo "All SBOMs validated successfully" + +# Upload all SBOMs to Dependency Track +# Requires DEPENDENCY_TRACK_API_KEY environment variable +.PHONY: upload-sboms +upload-sboms: + @if [ "$(HELM_SEMVER)" = "0.0.42" ]; then \ + echo "Environment variable HELM_SEMVER not set to non-default value. Re-run with HELM_SEMVER="; \ + exit 1; \ + fi + ./hack/bin/upload-all-sboms.sh $(DEPENDENCY_TRACK_PROJECT_NAME) "$(HELM_SEMVER)" + .PHONY: openapi-validate openapi-validate: @echo -e "Make sure you are running the backend in another terminal (make cr)\n" diff --git a/cassandra-schema.cql b/cassandra-schema.cql index 79ca5d700ab..7d2e47e16e0 100644 --- a/cassandra-schema.cql +++ b/cassandra-schema.cql @@ -14,744 +14,846 @@ CREATE TYPE brig_test.pubkey ( pem blob ); -CREATE TABLE brig_test.team_invitation_info ( - code ascii PRIMARY KEY, - id uuid, - team uuid -) WITH bloom_filter_fp_chance = 0.01 +CREATE TABLE brig_test.activation_keys ( + key ascii PRIMARY KEY, + challenge ascii, + code ascii, + key_text text, + key_type ascii, + retries int, + user uuid +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.provider_keys ( - key text PRIMARY KEY, - provider uuid -) WITH bloom_filter_fp_chance = 0.1 +CREATE TABLE brig_test.blacklist ( + key text PRIMARY KEY +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.oauth_refresh_token ( - id uuid PRIMARY KEY, - client uuid, - created_at timestamp, - scope set, - user uuid -) WITH bloom_filter_fp_chance = 0.01 +CREATE TABLE brig_test.budget ( + key text PRIMARY KEY, + budget int +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 14515200 - AND gc_grace_seconds = 864000 + AND default_time_to_live = 0 + AND extensions = {} + AND gc_grace_seconds = 0 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.team_invitation_email ( - email text, - team uuid, - code ascii, - invitation uuid, - PRIMARY KEY (email, team) -) WITH CLUSTERING ORDER BY (team ASC) +CREATE TABLE brig_test.clients ( + user uuid, + client text, + class int, + cookie text, + ip inet, + label text, + last_active timestamp, + lat double, + lon double, + model text, + tstamp timestamp, + type int, + capabilities set, + PRIMARY KEY (user, client) +) WITH CLUSTERING ORDER BY (client ASC) + AND additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.rich_info ( - user uuid PRIMARY KEY, - json blob -) WITH bloom_filter_fp_chance = 0.01 +CREATE TABLE brig_test.codes ( + user uuid, + scope int, + code text, + retries int, + PRIMARY KEY (user, scope) +) WITH CLUSTERING ORDER BY (scope ASC) + AND additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.service_tag ( - bucket int, - tag bigint, - name text, - service uuid, - provider uuid, - PRIMARY KEY ((bucket, tag), name, service) -) WITH CLUSTERING ORDER BY (name ASC, service ASC) +CREATE TABLE brig_test.connection ( + left uuid, + right uuid, + conv uuid, + last_update timestamp, + message text, + status int, + PRIMARY KEY (left, right) +) WITH CLUSTERING ORDER BY (right ASC) + AND additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.meta ( - id int, - version int, - date timestamp, - descr text, - PRIMARY KEY (id, version) -) WITH CLUSTERING ORDER BY (version ASC) - AND bloom_filter_fp_chance = 0.01 +CREATE INDEX conn_status ON brig_test.connection (status); + +CREATE TABLE brig_test.connection_remote ( + left uuid, + right_domain text, + right_user uuid, + conv_domain text, + conv_id uuid, + last_update timestamp, + status int, + PRIMARY KEY (left, right_domain, right_user) +) WITH CLUSTERING ORDER BY (right_domain ASC, right_user ASC) + AND additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.unique_claims ( - value text PRIMARY KEY, - claims set -) WITH bloom_filter_fp_chance = 0.1 +CREATE INDEX connection_remote_right_domain_idx ON brig_test.connection_remote (right_domain); + +CREATE TABLE brig_test.domain_registration ( + domain text PRIMARY KEY, + authorized_team uuid, + backend_url blob, + dns_verification_token ascii, + domain_redirect int, + idp_id uuid, + ownership_token_hash blob, + team uuid, + team_invite int, + webapp_url blob +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 - AND gc_grace_seconds = 0 + AND extensions = {} + AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.user_cookies ( - user uuid, - expires timestamp, - id bigint, - created timestamp, - label text, - succ_id bigint, - type int, - PRIMARY KEY (user, expires, id) -) WITH CLUSTERING ORDER BY (expires ASC, id ASC) - AND bloom_filter_fp_chance = 0.1 +CREATE TABLE brig_test.domain_registration_by_team ( + team uuid, + domain text, + PRIMARY KEY (team, domain) +) WITH CLUSTERING ORDER BY (domain ASC) + AND additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.mls_key_packages ( - user uuid, - client text, - cipher_suite int, - ref blob, - data blob, - PRIMARY KEY ((user, client, cipher_suite), ref) -) WITH CLUSTERING ORDER BY (ref ASC) - AND bloom_filter_fp_chance = 0.1 +CREATE TABLE brig_test.domain_registration_challenge ( + id uuid PRIMARY KEY, + challenge_token_hash blob, + dns_verification_token ascii, + domain text +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.mls_key_package_refs ( - ref blob PRIMARY KEY, - client text, - conv uuid, - conv_domain text, - domain text, - user uuid -) WITH bloom_filter_fp_chance = 0.1 +CREATE TABLE brig_test.excluded_phones ( + prefix text PRIMARY KEY, + comment text +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.oauth_client ( - id uuid PRIMARY KEY, - name text, - redirect_uri blob, - secret blob -) WITH bloom_filter_fp_chance = 0.01 +CREATE TABLE brig_test.federation_remote_teams ( + domain text, + team uuid, + PRIMARY KEY (domain, team) +) WITH CLUSTERING ORDER BY (team ASC) + AND additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.codes ( - user uuid, - scope int, - code text, - retries int, - PRIMARY KEY (user, scope) -) WITH CLUSTERING ORDER BY (scope ASC) +CREATE TABLE brig_test.federation_remotes ( + domain text PRIMARY KEY, + restriction int, + search_policy int +) WITH additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.user_handle ( - handle text PRIMARY KEY, - user uuid -) WITH bloom_filter_fp_chance = 0.1 +CREATE TABLE brig_test.invitee_info ( + invitee uuid PRIMARY KEY, + conv uuid, + inviter uuid +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.service ( - provider uuid, - id uuid, - assets list>, - auth_tokens list, - base_url blob, - descr text, - enabled boolean, - fingerprints list, - name text, - pubkeys list>, - summary text, - tags set, - PRIMARY KEY (provider, id) -) WITH CLUSTERING ORDER BY (id ASC) - AND bloom_filter_fp_chance = 0.1 +CREATE TABLE brig_test.login_codes ( + user uuid PRIMARY KEY, + code text, + retries int, + timeout timestamp +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.oauth_user_refresh_token ( - user uuid, - token_id uuid, - PRIMARY KEY (user, token_id) -) WITH CLUSTERING ORDER BY (token_id ASC) +CREATE TABLE brig_test.meta ( + id int, + version int, + date timestamp, + descr text, + PRIMARY KEY (id, version) +) WITH CLUSTERING ORDER BY (version ASC) + AND additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 14515200 + AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.domain_registration_challenge ( - id uuid PRIMARY KEY, - challenge_token_hash blob, - dns_verification_token ascii, - domain text -) WITH bloom_filter_fp_chance = 0.01 +CREATE TABLE brig_test.mls_key_package_refs ( + ref blob PRIMARY KEY, + client text, + conv uuid, + conv_domain text, + domain text, + user uuid +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.service_whitelist ( - team uuid, - provider uuid, - service uuid, - PRIMARY KEY (team, provider, service) -) WITH CLUSTERING ORDER BY (provider ASC, service ASC) - AND bloom_filter_fp_chance = 0.01 +CREATE TABLE brig_test.mls_key_packages ( + user uuid, + client text, + cipher_suite int, + ref blob, + data blob, + PRIMARY KEY ((user, client, cipher_suite), ref) +) WITH CLUSTERING ORDER BY (ref ASC) + AND additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.provider ( - id uuid PRIMARY KEY, - descr text, - email text, - name text, - password blob, - url blob -) WITH bloom_filter_fp_chance = 0.1 +CREATE TABLE brig_test.mls_public_keys ( + user uuid, + client text, + sig_scheme text, + key blob, + PRIMARY KEY (user, client, sig_scheme) +) WITH CLUSTERING ORDER BY (client ASC, sig_scheme ASC) + AND additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.user_keys ( - key text PRIMARY KEY, - user uuid -) WITH bloom_filter_fp_chance = 0.1 +CREATE TABLE brig_test.nonce ( + user uuid, + key text, + nonce uuid, + PRIMARY KEY (user, key) +) WITH CLUSTERING ORDER BY (key ASC) + AND additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 + AND default_time_to_live = 300 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.excluded_phones ( - prefix text PRIMARY KEY, - comment text -) WITH bloom_filter_fp_chance = 0.01 +CREATE TABLE brig_test.oauth_auth_code ( + code ascii PRIMARY KEY, + client uuid, + code_challenge blob, + redirect_uri blob, + user uuid, + scope set +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 + AND default_time_to_live = 300 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.mls_public_keys ( - user uuid, - client text, - sig_scheme text, - key blob, - PRIMARY KEY (user, client, sig_scheme) -) WITH CLUSTERING ORDER BY (client ASC, sig_scheme ASC) - AND bloom_filter_fp_chance = 0.1 +CREATE TABLE brig_test.oauth_client ( + id uuid PRIMARY KEY, + name text, + redirect_uri blob, + secret blob +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.invitee_info ( - invitee uuid PRIMARY KEY, - conv uuid, - inviter uuid -) WITH bloom_filter_fp_chance = 0.1 +CREATE TABLE brig_test.oauth_refresh_token ( + id uuid PRIMARY KEY, + client uuid, + created_at timestamp, + user uuid, + scope set +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 + AND default_time_to_live = 14515200 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.nonce ( +CREATE TABLE brig_test.oauth_user_refresh_token ( user uuid, - key text, - nonce uuid, - PRIMARY KEY (user, key) -) WITH CLUSTERING ORDER BY (key ASC) + token_id uuid, + PRIMARY KEY (user, token_id) +) WITH CLUSTERING ORDER BY (token_id ASC) + AND additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 300 + AND default_time_to_live = 14515200 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.login_codes ( - user uuid PRIMARY KEY, - code text, +CREATE TABLE brig_test.password_reset ( + key ascii PRIMARY KEY, + code ascii, retries int, - timeout timestamp -) WITH bloom_filter_fp_chance = 0.01 + timeout timestamp, + user uuid +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.federation_remote_teams ( - domain text, - team uuid, - PRIMARY KEY (domain, team) -) WITH CLUSTERING ORDER BY (team ASC) +CREATE TABLE brig_test.prekeys ( + user uuid, + client text, + key int, + data text, + PRIMARY KEY (user, client, key) +) WITH CLUSTERING ORDER BY (client ASC, key ASC) + AND additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.domain_registration ( - domain text PRIMARY KEY, - authorized_team uuid, - backend_url blob, - dns_verification_token ascii, - domain_redirect int, - idp_id uuid, - ownership_token_hash blob, - team uuid, - team_invite int, - webapp_url blob -) WITH bloom_filter_fp_chance = 0.01 +CREATE TABLE brig_test.properties ( + user uuid, + key ascii, + value blob, + PRIMARY KEY (user, key) +) WITH CLUSTERING ORDER BY (key ASC) + AND additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.service_team ( - provider uuid, - service uuid, - team uuid, - user uuid, - conv uuid, - PRIMARY KEY ((provider, service), team, user) -) WITH CLUSTERING ORDER BY (team ASC, user ASC) - AND bloom_filter_fp_chance = 0.01 +CREATE TABLE brig_test.provider ( + id uuid PRIMARY KEY, + descr text, + email text, + name text, + password blob, + url blob +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.blacklist ( - key text PRIMARY KEY -) WITH bloom_filter_fp_chance = 0.1 +CREATE TABLE brig_test.provider_keys ( + key text PRIMARY KEY, + provider uuid +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.service_whitelist_rev ( - provider uuid, - service uuid, - team uuid, - PRIMARY KEY ((provider, service), team) -) WITH CLUSTERING ORDER BY (team ASC) +CREATE TABLE brig_test.rich_info ( + user uuid PRIMARY KEY, + json blob +) WITH additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.team_invitation ( - team uuid, +CREATE TABLE brig_test.service ( + provider uuid, id uuid, - code ascii, - created_at timestamp, - created_by uuid, - email text, + base_url blob, + descr text, + enabled boolean, name text, - role int, - PRIMARY KEY (team, id) + summary text, + assets list>, + auth_tokens list, + fingerprints list, + pubkeys list>, + tags set, + PRIMARY KEY (provider, id) ) WITH CLUSTERING ORDER BY (id ASC) - AND bloom_filter_fp_chance = 0.01 + AND additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.user ( - id uuid PRIMARY KEY, - accent list, - accent_id int, - activated boolean, - assets list>, - country ascii, - email text, - email_unvalidated text, - expires timestamp, - feature_conference_calling int, - handle text, - language ascii, - managed_by int, +CREATE TABLE brig_test.service_prefix ( + prefix text, name text, - password blob, - picture list, - provider uuid, - searchable boolean, service uuid, - sso_id text, - status int, - supported_protocols int, - team uuid, - text_status text, - user_type int, - write_time_bumper int -) WITH bloom_filter_fp_chance = 0.1 + provider uuid, + PRIMARY KEY (prefix, name, service) +) WITH CLUSTERING ORDER BY (name ASC, service ASC) + AND additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.vcodes_throttle ( - key ascii, - scope int, - initial_delay int, - PRIMARY KEY (key, scope) -) WITH CLUSTERING ORDER BY (scope ASC) - AND bloom_filter_fp_chance = 0.01 +CREATE TABLE brig_test.service_tag ( + bucket int, + tag bigint, + name text, + service uuid, + provider uuid, + PRIMARY KEY ((bucket, tag), name, service) +) WITH CLUSTERING ORDER BY (name ASC, service ASC) + AND additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.properties ( +CREATE TABLE brig_test.service_team ( + provider uuid, + service uuid, + team uuid, user uuid, - key ascii, - value blob, - PRIMARY KEY (user, key) -) WITH CLUSTERING ORDER BY (key ASC) + conv uuid, + PRIMARY KEY ((provider, service), team, user) +) WITH CLUSTERING ORDER BY (team ASC, user ASC) + AND additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; CREATE TABLE brig_test.service_user ( provider uuid, @@ -761,245 +863,304 @@ CREATE TABLE brig_test.service_user ( team uuid, PRIMARY KEY ((provider, service), user) ) WITH CLUSTERING ORDER BY (user ASC) + AND additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.prekeys ( - user uuid, - client text, - key int, - data text, - PRIMARY KEY (user, client, key) -) WITH CLUSTERING ORDER BY (client ASC, key ASC) +CREATE TABLE brig_test.service_whitelist ( + team uuid, + provider uuid, + service uuid, + PRIMARY KEY (team, provider, service) +) WITH CLUSTERING ORDER BY (provider ASC, service ASC) + AND additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.oauth_auth_code ( - code ascii PRIMARY KEY, - client uuid, - code_challenge blob, - redirect_uri blob, - scope set, - user uuid -) WITH bloom_filter_fp_chance = 0.01 +CREATE TABLE brig_test.service_whitelist_rev ( + provider uuid, + service uuid, + team uuid, + PRIMARY KEY ((provider, service), team) +) WITH CLUSTERING ORDER BY (team ASC) + AND additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 300 + AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.clients ( - user uuid, - client text, - capabilities set, - class int, - cookie text, - ip inet, - label text, - last_active timestamp, - lat double, - lon double, - model text, - tstamp timestamp, - type int, - PRIMARY KEY (user, client) -) WITH CLUSTERING ORDER BY (client ASC) +CREATE TABLE brig_test.team_invitation ( + team uuid, + id uuid, + code ascii, + created_at timestamp, + created_by uuid, + email text, + name text, + role int, + PRIMARY KEY (team, id) +) WITH CLUSTERING ORDER BY (id ASC) + AND additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.budget ( - key text PRIMARY KEY, - budget int -) WITH bloom_filter_fp_chance = 0.1 +CREATE TABLE brig_test.team_invitation_email ( + email text, + team uuid, + code ascii, + invitation uuid, + PRIMARY KEY (email, team) +) WITH CLUSTERING ORDER BY (team ASC) + AND additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 - AND gc_grace_seconds = 0 + AND extensions = {} + AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.connection_remote ( - left uuid, - right_domain text, - right_user uuid, - conv_domain text, - conv_id uuid, - last_update timestamp, - status int, - PRIMARY KEY (left, right_domain, right_user) -) WITH CLUSTERING ORDER BY (right_domain ASC, right_user ASC) - AND bloom_filter_fp_chance = 0.1 +CREATE TABLE brig_test.team_invitation_info ( + code ascii PRIMARY KEY, + id uuid, + team uuid +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; -CREATE INDEX connection_remote_right_domain_idx ON brig_test.connection_remote (right_domain); + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.users_pending_activation ( - user uuid PRIMARY KEY, - expires_at timestamp -) WITH bloom_filter_fp_chance = 0.01 +CREATE TABLE brig_test.unique_claims ( + value text PRIMARY KEY, + claims set +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' 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 extensions = {} + AND gc_grace_seconds = 0 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.connection ( - left uuid, - right uuid, - conv uuid, - last_update timestamp, - message text, +CREATE TABLE brig_test.user ( + id uuid PRIMARY KEY, + accent_id int, + activated boolean, + country ascii, + email text, + email_unvalidated text, + expires timestamp, + feature_conference_calling int, + handle text, + language ascii, + managed_by int, + name text, + password blob, + provider uuid, + searchable boolean, + service uuid, + sso_id text, status int, - PRIMARY KEY (left, right) -) WITH CLUSTERING ORDER BY (right ASC) + supported_protocols int, + team uuid, + text_status text, + user_type int, + write_time_bumper int, + accent list, + assets list>, + picture list +) WITH additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; -CREATE INDEX conn_status ON brig_test.connection (status); + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.federation_remotes ( - domain text PRIMARY KEY, - restriction int, - search_policy int -) WITH bloom_filter_fp_chance = 0.01 +CREATE TABLE brig_test.user_cookies ( + user uuid, + expires timestamp, + id bigint, + created timestamp, + label text, + succ_id bigint, + type int, + PRIMARY KEY (user, expires, id) +) WITH CLUSTERING ORDER BY (expires ASC, id ASC) + AND additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.password_reset ( - key ascii PRIMARY KEY, - code ascii, - retries int, - timeout timestamp, +CREATE TABLE brig_test.user_handle ( + handle text PRIMARY KEY, user uuid -) WITH bloom_filter_fp_chance = 0.1 +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.activation_keys ( - key ascii PRIMARY KEY, - challenge ascii, - code ascii, - key_text text, - key_type ascii, - retries int, +CREATE TABLE brig_test.user_keys ( + key text PRIMARY KEY, user uuid -) WITH bloom_filter_fp_chance = 0.1 +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' + AND crc_check_chance = 1.0 + AND default_time_to_live = 0 + AND extensions = {} + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; + +CREATE TABLE brig_test.users_pending_activation ( + user uuid PRIMARY KEY, + expires_at timestamp +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; CREATE TABLE brig_test.vcodes ( key ascii, @@ -1011,62 +1172,47 @@ CREATE TABLE brig_test.vcodes ( value ascii, PRIMARY KEY (key, scope) ) WITH CLUSTERING ORDER BY (scope ASC) + AND additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 0 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE brig_test.service_prefix ( - prefix text, - name text, - service uuid, - provider uuid, - PRIMARY KEY (prefix, name, service) -) WITH CLUSTERING ORDER BY (name ASC, service ASC) - AND bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE brig_test.domain_registration_by_team ( - team uuid, - domain text, - PRIMARY KEY (team, domain) -) WITH CLUSTERING ORDER BY (domain ASC) +CREATE TABLE brig_test.vcodes_throttle ( + key ascii, + scope int, + initial_delay int, + PRIMARY KEY (key, scope) +) WITH CLUSTERING ORDER BY (scope ASC) + AND additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; CREATE KEYSPACE galley_test WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} AND durable_writes = true; CREATE TYPE galley_test.permissions ( @@ -1080,157 +1226,158 @@ CREATE TYPE galley_test.pubkey ( pem blob ); -CREATE TABLE galley_test.meta ( - id int, - version int, - date timestamp, - descr text, - PRIMARY KEY (id, version) -) WITH CLUSTERING ORDER BY (version ASC) +CREATE TABLE galley_test.billing_team_member ( + team uuid, + user uuid, + PRIMARY KEY (team, user) +) WITH CLUSTERING ORDER BY (user ASC) + AND additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE galley_test.team_conv ( - team uuid, - conv uuid, - PRIMARY KEY (team, conv) -) WITH CLUSTERING ORDER BY (conv ASC) - AND bloom_filter_fp_chance = 0.1 +CREATE TABLE galley_test.clients ( + user uuid PRIMARY KEY, + clients set +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 - AND gc_grace_seconds = 86400 + AND extensions = {} + AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE galley_test.user_team ( - user uuid, +CREATE TABLE galley_test.conversation ( + conv uuid PRIMARY KEY, + access_role int, + cells_state int, + channel_add_permission int, + cipher_suite int, + creator uuid, + deleted boolean, + epoch bigint, + group_conv_type int, + group_id blob, + history_depth bigint, + message_timer bigint, + name text, + parent_conv uuid, + protocol int, + public_group_state blob, + receipt_mode int, team uuid, - PRIMARY KEY (user, team) -) WITH CLUSTERING ORDER BY (team ASC) + type int, + access set, + access_roles_v2 set +) WITH additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' 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 extensions = {} + AND gc_grace_seconds = 86400 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE galley_test.team_features ( - team_id uuid PRIMARY KEY, - app_lock_enforce int, - app_lock_inactivity_timeout_secs int, - app_lock_status int, - conference_calling int, - conference_calling_one_to_one int, - conference_calling_status int, - digital_signatures int, - domain_registration_lock_status int, - domain_registration_status int, - enforce_file_download_location text, - enforce_file_download_location_lock_status int, - enforce_file_download_location_status int, - expose_invitation_urls_to_team_admin int, - file_sharing int, - file_sharing_lock_status int, - guest_links_lock_status int, - guest_links_status int, - legalhold_status int, - limited_event_fanout_status int, - migration_state int, - mls_allowed_ciphersuites set, - mls_default_ciphersuite int, - mls_default_protocol int, - mls_e2eid_acme_discovery_url blob, - mls_e2eid_crl_proxy blob, - mls_e2eid_grace_period int, - mls_e2eid_lock_status int, - mls_e2eid_status int, - mls_e2eid_use_proxy_on_mobile boolean, - mls_e2eid_ver_exp timestamp, - mls_lock_status int, - mls_migration_finalise_regardless_after timestamp, - mls_migration_lock_status int, - mls_migration_start_time timestamp, - mls_migration_status int, - mls_protocol_toggle_users set, - mls_status int, - mls_supported_protocols set, - outlook_cal_integration_lock_status int, - outlook_cal_integration_status int, - search_visibility_inbound_status int, - search_visibility_status int, - self_deleting_messages_lock_status int, - self_deleting_messages_status int, - self_deleting_messages_ttl int, - snd_factor_password_challenge_lock_status int, - snd_factor_password_challenge_status int, - sso_status int, - validate_saml_emails int -) WITH bloom_filter_fp_chance = 0.1 +CREATE TABLE galley_test.conversation_codes ( + key ascii, + scope int, + conversation uuid, + password blob, + value ascii, + PRIMARY KEY (key, scope) +) WITH CLUSTERING ORDER BY (scope ASC) + AND additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE galley_test.service ( - provider uuid, - id uuid, - auth_token ascii, - base_url blob, - enabled boolean, - fingerprints set, - PRIMARY KEY (provider, id) -) WITH CLUSTERING ORDER BY (id ASC) +CREATE TABLE galley_test.conversation_out_of_sync ( + conv_id uuid PRIMARY KEY, + out_of_sync boolean +) WITH additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; + +CREATE TABLE galley_test.custom_backend ( + domain text PRIMARY KEY, + config_json_url blob, + webapp_welcome_url blob +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' + AND crc_check_chance = 1.0 + AND default_time_to_live = 0 + AND extensions = {} + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; CREATE TABLE galley_test.data_migration ( id int, @@ -1239,133 +1386,148 @@ CREATE TABLE galley_test.data_migration ( descr text, PRIMARY KEY (id, version) ) WITH CLUSTERING ORDER BY (version ASC) + AND additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE galley_test.conversation_out_of_sync ( - conv_id uuid PRIMARY KEY, - out_of_sync boolean -) WITH bloom_filter_fp_chance = 0.01 +CREATE TABLE galley_test.group_id_conv_id ( + group_id blob PRIMARY KEY, + conv_id uuid, + domain text, + subconv_id text +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE galley_test.member ( - conv uuid, +CREATE TABLE galley_test.legalhold_pending_prekeys ( user uuid, - conversation_role text, - hidden boolean, - hidden_ref text, - otr_archived boolean, - otr_archived_ref text, - otr_muted boolean, - otr_muted_ref text, - otr_muted_status int, - provider uuid, - service uuid, - status int, - PRIMARY KEY (conv, user) -) WITH CLUSTERING ORDER BY (user ASC) + key int, + data text, + PRIMARY KEY (user, key) +) WITH CLUSTERING ORDER BY (key ASC) + AND additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 - AND gc_grace_seconds = 86400 + AND extensions = {} + AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE galley_test.custom_backend ( - domain text PRIMARY KEY, - config_json_url blob, - webapp_welcome_url blob -) WITH bloom_filter_fp_chance = 0.01 +CREATE TABLE galley_test.legalhold_service ( + team_id uuid PRIMARY KEY, + auth_token ascii, + base_url blob, + fingerprint blob, + pubkey pubkey +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE galley_test.user_remote_conv ( - user uuid, - conv_remote_domain text, - conv_remote_id uuid, - hidden boolean, - hidden_ref text, - otr_archived boolean, - otr_archived_ref text, - otr_muted_ref text, - otr_muted_status int, - PRIMARY KEY (user, conv_remote_domain, conv_remote_id) -) WITH CLUSTERING ORDER BY (conv_remote_domain ASC, conv_remote_id ASC) - AND bloom_filter_fp_chance = 0.1 +CREATE TABLE galley_test.legalhold_whitelisted ( + team uuid PRIMARY KEY +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; -CREATE INDEX user_remote_conv_conv_remote_domain_idx ON galley_test.user_remote_conv (conv_remote_domain); + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE galley_test.legalhold_whitelisted ( - team uuid PRIMARY KEY -) WITH bloom_filter_fp_chance = 0.01 +CREATE TABLE galley_test.member ( + conv uuid, + user uuid, + conversation_role text, + hidden boolean, + hidden_ref text, + otr_archived boolean, + otr_archived_ref text, + otr_muted boolean, + otr_muted_ref text, + otr_muted_status int, + provider uuid, + service uuid, + status int, + PRIMARY KEY (conv, user) +) WITH CLUSTERING ORDER BY (user ASC) + AND additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' 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 extensions = {} + AND gc_grace_seconds = 86400 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; CREATE TABLE galley_test.member_remote_user ( conv uuid, @@ -1374,406 +1536,480 @@ CREATE TABLE galley_test.member_remote_user ( conversation_role text, PRIMARY KEY (conv, user_remote_domain, user_remote_id) ) WITH CLUSTERING ORDER BY (user_remote_domain ASC, user_remote_id ASC) + AND additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 86400 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; + CREATE INDEX member_remote_user_user_remote_domain_idx ON galley_test.member_remote_user (user_remote_domain); -CREATE TABLE galley_test.team_member ( - team uuid, - user uuid, - invited_at timestamp, - invited_by uuid, - legalhold_status int, - perms frozen, - PRIMARY KEY (team, user) -) WITH CLUSTERING ORDER BY (user ASC) - AND bloom_filter_fp_chance = 0.1 +CREATE TABLE galley_test.meta ( + id int, + version int, + date timestamp, + descr text, + PRIMARY KEY (id, version) +) WITH CLUSTERING ORDER BY (version ASC) + AND additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE galley_test.team_notifications ( - team uuid, - id timeuuid, - payload blob, - PRIMARY KEY (team, id) -) WITH CLUSTERING ORDER BY (id ASC) - AND bloom_filter_fp_chance = 0.1 +CREATE TABLE galley_test.mls_commit_locks ( + group_id blob, + epoch bigint, + PRIMARY KEY (group_id, epoch) +) WITH CLUSTERING ORDER BY (epoch ASC) + AND additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE galley_test.legalhold_pending_prekeys ( +CREATE TABLE galley_test.mls_group_member_client ( + group_id blob, + user_domain text, user uuid, - key int, - data text, - PRIMARY KEY (user, key) -) WITH CLUSTERING ORDER BY (key ASC) - AND bloom_filter_fp_chance = 0.1 + client text, + key_package_ref blob, + leaf_node_index int, + removal_pending boolean, + PRIMARY KEY (group_id, user_domain, user, client) +) WITH CLUSTERING ORDER BY (user_domain ASC, user ASC, client ASC) + AND additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' 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 extensions = {} + AND gc_grace_seconds = 86400 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE galley_test.group_id_conv_id ( - group_id blob PRIMARY KEY, - conv_id uuid, - domain text, - subconv_id text -) WITH bloom_filter_fp_chance = 0.01 +CREATE TABLE galley_test.mls_proposal_refs ( + group_id blob, + epoch bigint, + ref blob, + origin int, + proposal blob, + PRIMARY KEY (group_id, epoch, ref) +) WITH CLUSTERING ORDER BY (epoch ASC, ref ASC) + AND additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE galley_test.team_admin ( - team uuid, - user uuid, - PRIMARY KEY (team, user) -) WITH CLUSTERING ORDER BY (user ASC) - AND bloom_filter_fp_chance = 0.1 +CREATE TABLE galley_test.service ( + provider uuid, + id uuid, + auth_token ascii, + base_url blob, + enabled boolean, + fingerprints set, + PRIMARY KEY (provider, id) +) WITH CLUSTERING ORDER BY (id ASC) + AND additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE galley_test.user ( - user uuid, - conv uuid, - PRIMARY KEY (user, conv) -) WITH CLUSTERING ORDER BY (conv ASC) - AND bloom_filter_fp_chance = 0.1 +CREATE TABLE galley_test.subconversation ( + conv_id uuid, + subconv_id text, + cipher_suite int, + epoch bigint, + group_id blob, + public_group_state blob, + PRIMARY KEY (conv_id, subconv_id) +) WITH CLUSTERING ORDER BY (subconv_id ASC) + AND additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 86400 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE galley_test.legalhold_service ( - team_id uuid PRIMARY KEY, - auth_token ascii, - base_url blob, - fingerprint blob, - pubkey pubkey -) WITH bloom_filter_fp_chance = 0.1 +CREATE TABLE galley_test.team ( + team uuid PRIMARY KEY, + binding boolean, + creator uuid, + deleted boolean, + icon text, + icon_key text, + name text, + search_visibility int, + splash_screen text, + status int +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE galley_test.team_features_dyn ( +CREATE TABLE galley_test.team_admin ( team uuid, - feature text, - config text, - lock_status int, - status int, - PRIMARY KEY (team, feature) -) WITH CLUSTERING ORDER BY (feature ASC) + user uuid, + PRIMARY KEY (team, user) +) WITH CLUSTERING ORDER BY (user ASC) + AND additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE galley_test.conversation_codes ( - key ascii, - scope int, - conversation uuid, - password blob, - value ascii, - PRIMARY KEY (key, scope) -) WITH CLUSTERING ORDER BY (scope ASC) +CREATE TABLE galley_test.team_conv ( + team uuid, + conv uuid, + PRIMARY KEY (team, conv) +) WITH CLUSTERING ORDER BY (conv ASC) + AND additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE galley_test.mls_group_member_client ( - group_id blob, - user_domain text, - user uuid, - client text, - key_package_ref blob, - leaf_node_index int, - removal_pending boolean, - PRIMARY KEY (group_id, user_domain, user, client) -) WITH CLUSTERING ORDER BY (user_domain ASC, user ASC, client ASC) - AND bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 86400 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE galley_test.clients ( - user uuid PRIMARY KEY, - clients set -) WITH bloom_filter_fp_chance = 0.01 +CREATE TABLE galley_test.team_features ( + team_id uuid PRIMARY KEY, + app_lock_enforce int, + app_lock_inactivity_timeout_secs int, + app_lock_status int, + conference_calling int, + conference_calling_one_to_one int, + conference_calling_status int, + digital_signatures int, + domain_registration_lock_status int, + domain_registration_status int, + enforce_file_download_location text, + enforce_file_download_location_lock_status int, + enforce_file_download_location_status int, + expose_invitation_urls_to_team_admin int, + file_sharing int, + file_sharing_lock_status int, + guest_links_lock_status int, + guest_links_status int, + legalhold_status int, + limited_event_fanout_status int, + migration_state int, + mls_default_ciphersuite int, + mls_default_protocol int, + mls_e2eid_acme_discovery_url blob, + mls_e2eid_crl_proxy blob, + mls_e2eid_grace_period int, + mls_e2eid_lock_status int, + mls_e2eid_status int, + mls_e2eid_use_proxy_on_mobile boolean, + mls_e2eid_ver_exp timestamp, + mls_lock_status int, + mls_migration_finalise_regardless_after timestamp, + mls_migration_lock_status int, + mls_migration_start_time timestamp, + mls_migration_status int, + mls_status int, + outlook_cal_integration_lock_status int, + outlook_cal_integration_status int, + search_visibility_inbound_status int, + search_visibility_status int, + self_deleting_messages_lock_status int, + self_deleting_messages_status int, + self_deleting_messages_ttl int, + snd_factor_password_challenge_lock_status int, + snd_factor_password_challenge_status int, + sso_status int, + validate_saml_emails int, + mls_allowed_ciphersuites set, + mls_protocol_toggle_users set, + mls_supported_protocols set +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE galley_test.conversation ( - conv uuid PRIMARY KEY, - access set, - access_role int, - access_roles_v2 set, - cells_state int, - channel_add_permission int, - cipher_suite int, - creator uuid, - deleted boolean, - epoch bigint, - group_conv_type int, - group_id blob, - message_timer bigint, - name text, - parent_conv uuid, - protocol int, - public_group_state blob, - receipt_mode int, +CREATE TABLE galley_test.team_features_dyn ( team uuid, - type int -) WITH bloom_filter_fp_chance = 0.1 + feature text, + config text, + lock_status int, + status int, + PRIMARY KEY (team, feature) +) WITH CLUSTERING ORDER BY (feature ASC) + AND additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 - AND gc_grace_seconds = 86400 + AND extensions = {} + AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE galley_test.mls_commit_locks ( - group_id blob, - epoch bigint, - PRIMARY KEY (group_id, epoch) -) WITH CLUSTERING ORDER BY (epoch ASC) - AND bloom_filter_fp_chance = 0.01 +CREATE TABLE galley_test.team_member ( + team uuid, + user uuid, + invited_at timestamp, + invited_by uuid, + legalhold_status int, + perms frozen, + PRIMARY KEY (team, user) +) WITH CLUSTERING ORDER BY (user ASC) + AND additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE galley_test.subconversation ( - conv_id uuid, - subconv_id text, - cipher_suite int, - epoch bigint, - group_id blob, - public_group_state blob, - PRIMARY KEY (conv_id, subconv_id) -) WITH CLUSTERING ORDER BY (subconv_id ASC) - AND bloom_filter_fp_chance = 0.01 +CREATE TABLE galley_test.team_notifications ( + team uuid, + id timeuuid, + payload blob, + PRIMARY KEY (team, id) +) WITH CLUSTERING ORDER BY (id ASC) + AND additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 - AND gc_grace_seconds = 86400 + AND extensions = {} + AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE galley_test.team ( - team uuid PRIMARY KEY, - binding boolean, - creator uuid, - deleted boolean, - icon text, - icon_key text, - name text, - search_visibility int, - splash_screen text, - status int -) WITH bloom_filter_fp_chance = 0.1 +CREATE TABLE galley_test.user ( + user uuid, + conv uuid, + PRIMARY KEY (user, conv) +) WITH CLUSTERING ORDER BY (conv ASC) + AND additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' 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 extensions = {} + AND gc_grace_seconds = 86400 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE galley_test.billing_team_member ( - team uuid, +CREATE TABLE galley_test.user_remote_conv ( user uuid, - PRIMARY KEY (team, user) -) WITH CLUSTERING ORDER BY (user ASC) - AND bloom_filter_fp_chance = 0.01 + conv_remote_domain text, + conv_remote_id uuid, + hidden boolean, + hidden_ref text, + otr_archived boolean, + otr_archived_ref text, + otr_muted_ref text, + otr_muted_status int, + PRIMARY KEY (user, conv_remote_domain, conv_remote_id) +) WITH CLUSTERING ORDER BY (conv_remote_domain ASC, conv_remote_id ASC) + AND additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE galley_test.mls_proposal_refs ( - group_id blob, - epoch bigint, - ref blob, - origin int, - proposal blob, - PRIMARY KEY (group_id, epoch, ref) -) WITH CLUSTERING ORDER BY (epoch ASC, ref ASC) - AND bloom_filter_fp_chance = 0.01 +CREATE INDEX user_remote_conv_conv_remote_domain_idx ON galley_test.user_remote_conv (conv_remote_domain); + +CREATE TABLE galley_test.user_team ( + user uuid, + team uuid, + PRIMARY KEY (user, team) +) WITH CLUSTERING ORDER BY (team ASC) + AND additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; CREATE KEYSPACE gundeck_test WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} AND durable_writes = true; CREATE TABLE gundeck_test.data_migration ( @@ -1783,44 +2019,23 @@ CREATE TABLE gundeck_test.data_migration ( descr text, PRIMARY KEY (id, version) ) WITH CLUSTERING ORDER BY (version ASC) + AND additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE gundeck_test.notifications ( - user uuid, - id timeuuid, - clients set, - payload blob, - payload_ref uuid, - payload_ref_size int, - PRIMARY KEY (user, id) -) WITH CLUSTERING ORDER BY (id ASC) - AND bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy', 'compaction_window_size': '1', 'compaction_window_unit': 'DAYS', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 0 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; CREATE TABLE gundeck_test.meta ( id int, @@ -1829,40 +2044,94 @@ CREATE TABLE gundeck_test.meta ( descr text, PRIMARY KEY (id, version) ) WITH CLUSTERING ORDER BY (version ASC) + AND additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; CREATE TABLE gundeck_test.missed_notifications ( user_id uuid, client_id text, PRIMARY KEY (user_id, client_id) ) WITH CLUSTERING ORDER BY (client_id ASC) + AND additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' + AND crc_check_chance = 1.0 + AND default_time_to_live = 0 + AND extensions = {} + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; + +CREATE TABLE gundeck_test.notification_payload ( + id uuid PRIMARY KEY, + payload blob +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; + +CREATE TABLE gundeck_test.notifications ( + user uuid, + id timeuuid, + payload blob, + payload_ref uuid, + payload_ref_size int, + clients set, + PRIMARY KEY (user, id) +) WITH CLUSTERING ORDER BY (id ASC) + AND additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy', 'compaction_window_size': '1', 'compaction_window_unit': 'DAYS', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' + AND crc_check_chance = 1.0 + AND default_time_to_live = 0 + AND extensions = {} + AND gc_grace_seconds = 0 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; CREATE TABLE gundeck_test.push ( ptoken text, @@ -1873,20 +2142,23 @@ CREATE TABLE gundeck_test.push ( usr uuid, PRIMARY KEY (ptoken, app, transport) ) WITH CLUSTERING ORDER BY (app ASC, transport ASC) + AND additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; CREATE TABLE gundeck_test.user_push ( usr uuid, @@ -1898,79 +2170,88 @@ CREATE TABLE gundeck_test.user_push ( connection blob, PRIMARY KEY (usr, ptoken, app, transport) ) WITH CLUSTERING ORDER BY (ptoken ASC, app ASC, transport ASC) + AND additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; +CREATE KEYSPACE spar_test WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} AND durable_writes = true; -CREATE TABLE gundeck_test.notification_payload ( - id uuid PRIMARY KEY, - payload blob -) WITH bloom_filter_fp_chance = 0.1 +CREATE TABLE spar_test.authreq ( + req text PRIMARY KEY, + end_of_life timestamp, + idp_issuer text +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; -CREATE KEYSPACE spar_test WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} AND durable_writes = true; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE spar_test.bind_cookie ( - cookie text PRIMARY KEY, - session_owner uuid -) WITH bloom_filter_fp_chance = 0.01 +CREATE TABLE spar_test.authresp ( + resp text PRIMARY KEY, + end_of_life timestamp +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE spar_test.user_v2 ( - issuer text, - normalized_uname_id text, - sso_id text, - uid uuid, - PRIMARY KEY (issuer, normalized_uname_id) -) WITH CLUSTERING ORDER BY (normalized_uname_id ASC) - AND bloom_filter_fp_chance = 0.1 +CREATE TABLE spar_test.bind_cookie ( + cookie text PRIMARY KEY, + session_owner uuid +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; CREATE TABLE spar_test.data_migration ( id int, @@ -1979,146 +2260,142 @@ CREATE TABLE spar_test.data_migration ( descr text, PRIMARY KEY (id, version) ) WITH CLUSTERING ORDER BY (version ASC) + AND additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE spar_test.authresp ( - resp text PRIMARY KEY, - end_of_life timestamp -) WITH bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - -CREATE TABLE spar_test.idp_raw_metadata ( - id uuid PRIMARY KEY, - metadata text -) WITH bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE spar_test.issuer_idp ( - issuer text PRIMARY KEY, - idp uuid -) WITH bloom_filter_fp_chance = 0.1 +CREATE TABLE spar_test.default_idp ( + partition_key_always_default text, + idp uuid, + PRIMARY KEY (partition_key_always_default, idp) +) WITH CLUSTERING ORDER BY (idp ASC) + AND additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; CREATE TABLE spar_test.idp ( idp uuid PRIMARY KEY, api_version int, domain text, - extra_public_keys list, handle text, issuer text, - old_issuers list, public_key blob, replaced_by uuid, request_uri text, - team uuid -) WITH bloom_filter_fp_chance = 0.1 + team uuid, + extra_public_keys list, + old_issuers list +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE spar_test.default_idp ( - partition_key_always_default text, - idp uuid, - PRIMARY KEY (partition_key_always_default, idp) -) WITH CLUSTERING ORDER BY (idp ASC) +CREATE TABLE spar_test.idp_raw_metadata ( + id uuid PRIMARY KEY, + metadata text +) WITH additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE spar_test.team_provisioning_by_team ( +CREATE TABLE spar_test.issuer_idp ( + issuer text PRIMARY KEY, + idp uuid +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' + AND crc_check_chance = 1.0 + AND default_time_to_live = 0 + AND extensions = {} + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; + +CREATE TABLE spar_test.issuer_idp_v2 ( + issuer text, team uuid, - id uuid, - created_at timestamp, - descr text, idp uuid, - name text, - token_ text, - PRIMARY KEY (team, id) -) WITH CLUSTERING ORDER BY (id ASC) + PRIMARY KEY (issuer, team) +) WITH CLUSTERING ORDER BY (team ASC) + AND additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; CREATE TABLE spar_test.meta ( id int, @@ -2127,183 +2404,217 @@ CREATE TABLE spar_test.meta ( descr text, PRIMARY KEY (id, version) ) WITH CLUSTERING ORDER BY (version ASC) + AND additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE spar_test.verdict ( - req text PRIMARY KEY, - cookie_label text, - format_con int, - format_mobile_error text, - format_mobile_success text -) WITH bloom_filter_fp_chance = 0.1 +CREATE TABLE spar_test.scim_external ( + team uuid, + external_id text, + creation_status int, + user uuid, + PRIMARY KEY (team, external_id) +) WITH CLUSTERING ORDER BY (external_id ASC) + AND additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE spar_test.authreq ( - req text PRIMARY KEY, - end_of_life timestamp, - idp_issuer text -) WITH bloom_filter_fp_chance = 0.1 +CREATE TABLE spar_test.scim_user_times ( + uid uuid PRIMARY KEY, + created_at timestamp, + last_updated_at timestamp +) WITH additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE spar_test.team_provisioning_by_token ( - token_ text PRIMARY KEY, - created_at timestamp, - descr text, - id uuid, +CREATE TABLE spar_test.team_idp ( + team uuid, idp uuid, - name text, - team uuid -) WITH bloom_filter_fp_chance = 0.1 + PRIMARY KEY (team, idp) +) WITH CLUSTERING ORDER BY (idp ASC) + AND additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE spar_test.team_idp ( +CREATE TABLE spar_test.team_provisioning_by_team ( team uuid, + id uuid, + created_at timestamp, + descr text, idp uuid, - PRIMARY KEY (team, idp) -) WITH CLUSTERING ORDER BY (idp ASC) + name text, + token_ text, + PRIMARY KEY (team, id) +) WITH CLUSTERING ORDER BY (id ASC) + AND additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE spar_test.issuer_idp_v2 ( - issuer text, - team uuid, +CREATE TABLE spar_test.team_provisioning_by_token ( + token_ text PRIMARY KEY, + created_at timestamp, + descr text, + id uuid, idp uuid, - PRIMARY KEY (issuer, team) -) WITH CLUSTERING ORDER BY (team ASC) + name text, + team uuid +) WITH additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE spar_test.scim_user_times ( - uid uuid PRIMARY KEY, - created_at timestamp, - last_updated_at timestamp -) WITH bloom_filter_fp_chance = 0.1 +CREATE TABLE spar_test.user ( + issuer text, + sso_id text, + uid uuid, + PRIMARY KEY (issuer, sso_id) +) WITH CLUSTERING ORDER BY (sso_id ASC) + AND additional_write_policy = '99p' + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE spar_test.scim_external ( - team uuid, - external_id text, - creation_status int, - user uuid, - PRIMARY KEY (team, external_id) -) WITH CLUSTERING ORDER BY (external_id ASC) +CREATE TABLE spar_test.user_v2 ( + issuer text, + normalized_uname_id text, + sso_id text, + uid uuid, + PRIMARY KEY (issuer, normalized_uname_id) +) WITH CLUSTERING ORDER BY (normalized_uname_id ASC) + AND additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; -CREATE TABLE spar_test.user ( - issuer text, - sso_id text, - uid uuid, - PRIMARY KEY (issuer, sso_id) -) WITH CLUSTERING ORDER BY (sso_id ASC) +CREATE TABLE spar_test.verdict ( + req text PRIMARY KEY, + cookie_label text, + format_con int, + format_mobile_error text, + format_mobile_success text +) WITH additional_write_policy = '99p' AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND cdc = false AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND memtable = 'default' AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 + AND extensions = {} AND gc_grace_seconds = 864000 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; + AND read_repair = 'BLOCKING' + AND speculative_retry = '99p'; diff --git a/changelog.d/mk-changelog.sh b/changelog.d/mk-changelog.sh index c9616788dce..aa8cc3fd3bd 100755 --- a/changelog.d/mk-changelog.sh +++ b/changelog.d/mk-changelog.sh @@ -5,9 +5,12 @@ shopt -s nullglob DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -getPRNumber() { - git log --reverse --format=%s -- "$1" | sed -rn '1 { /\((#.*)\)$/ s|^.*\((#.*)\)$|\1|p; }' | grep "" || +get_pr_number() { + file="$1" + git log --reverse --format=%s -- "$file" | sed -rn '1 { /\((#.*)\)$/ s|^.*\((#.*)\)$|\1|p; }' | grep "" || echo "#PR_NOT_FOUND" + + return 0 } for d in "$DIR"/*; do @@ -23,7 +26,7 @@ for d in "$DIR"/*; do echo "" # shellcheck disable=SC2094 for f in "${entries[@]}"; do - pr=$(getPRNumber "$f") + pr=$(get_pr_number "$f") # shellcheck disable=SC1003 < "$f" sed -r ' # create a bullet point on the first line diff --git a/charts/background-worker/Chart.yaml b/charts/background-worker/Chart.yaml deleted file mode 100644 index c3f182478c5..00000000000 --- a/charts/background-worker/Chart.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -description: Backend notification pusher -name: background-worker -version: 0.0.42 diff --git a/charts/background-worker/README.md b/charts/background-worker/README.md deleted file mode 100644 index b3ef345c5f5..00000000000 --- a/charts/background-worker/README.md +++ /dev/null @@ -1,21 +0,0 @@ -Note that background-worker depends on some provisioned storage/services, namely: - -- rabbitmq -- postgresql -- cassandra (three clusters) - -PostgreSQL configuration -- Set connection parameters under `config.postgresql` (libpq keywords: `host`, `port`, `user`, `dbname`, etc.). -- Provide the password via `secrets.pgPassword`; it is mounted at `/etc/wire/background-worker/secrets/pgPassword` and referenced from the configmap. - -Cassandra configuration -- Background-worker connects to three Cassandra clusters: - - `config.cassandra` (keyspace: `gundeck`) for the dead user notification watcher. - - `config.cassandraBrig` (keyspace: `brig`) for the user store. - - `config.cassandraGalley` (keyspace: `galley`) for conversation-related data access. -- TLS may be configured via either a reference (`tlsCaSecretRef`) or inline CA (`tlsCa`) for each cluster. Secrets mount under: - - `/etc/wire/background-worker/cassandra-gundeck` - - `/etc/wire/background-worker/cassandra-brig` - - `/etc/wire/background-worker/cassandra-galley` - -These are dealt with independently from this chart. diff --git a/charts/background-worker/templates/_helpers.tpl b/charts/background-worker/templates/_helpers.tpl deleted file mode 100644 index 2d576ac1412..00000000000 --- a/charts/background-worker/templates/_helpers.tpl +++ /dev/null @@ -1,50 +0,0 @@ - -{{/* Allow KubeVersion to be overridden. */}} -{{- define "kubeVersion" -}} - {{- default .Capabilities.KubeVersion.Version .Values.kubeVersionOverride -}} -{{- end -}} - -{{- define "includeSecurityContext" -}} - {{- (semverCompare ">= 1.24-0" (include "kubeVersion" .)) -}} -{{- end -}} - -{{- define "useGundeckCassandraTLS" -}} -{{ or (hasKey .cassandra "tlsCa") (hasKey .cassandra "tlsCaSecretRef") }} -{{- end -}} - -{{- define "useBrigCassandraTLS" -}} -{{ or (hasKey .cassandraBrig "tlsCa") (hasKey .cassandraBrig "tlsCaSecretRef") }} -{{- end -}} - -{{- define "useGalleyCassandraTLS" -}} -{{ or (hasKey .cassandraGalley "tlsCa") (hasKey .cassandraGalley "tlsCaSecretRef") }} -{{- end -}} - -{{- define "gundeckTlsSecretRef" -}} -{{- if .cassandra.tlsCaSecretRef -}} -{{ .cassandra.tlsCaSecretRef | toYaml }} -{{- else }} -{{- dict "name" "background-worker-cassandra-gundeck" "key" "ca.pem" | toYaml -}} -{{- end -}} -{{- end -}} - -{{- define "brigTlsSecretRef" -}} -{{- if .cassandraBrig.tlsCaSecretRef -}} -{{ .cassandraBrig.tlsCaSecretRef | toYaml }} -{{- else }} -{{- dict "name" "background-worker-cassandra-brig" "key" "ca.pem" | toYaml -}} -{{- end -}} -{{- end -}} - -{{/* Return a Dict of TLS CA secret name and key -This is used to switch between provided secret (e.g. by cert-manager) and -created one (in case the CA is provided as PEM string.) -*/}} -{{- define "galleyTlsSecretRef" -}} -{{- if and .cassandraGalley .cassandraGalley.tlsCaSecretRef -}} -{{ .cassandraGalley.tlsCaSecretRef | toYaml }} -{{- else }} -{{- dict "name" "background-worker-cassandra-galley" "key" "ca.pem" | toYaml -}} -{{- end -}} -{{- end -}} - diff --git a/charts/background-worker/values.yaml b/charts/background-worker/values.yaml deleted file mode 100644 index 4e8fe290473..00000000000 --- a/charts/background-worker/values.yaml +++ /dev/null @@ -1,127 +0,0 @@ -replicaCount: 1 -image: - repository: quay.io/wire/background-worker - tag: do-not-use -service: - internalPort: 8080 - externalPort: 8080 -# FUTUREWORK: Review these values when we have some experience -resources: - requests: - memory: "200Mi" - cpu: "100m" - limits: - memory: "512Mi" -metrics: - serviceMonitor: - enabled: false -config: - logLevel: Info - logFormat: StructuredJSON - enableFederation: false # keep in sync with brig, cargohold and galley charts' config.enableFederation as well as wire-server chart's tags.federation - brig: - host: brig - port: 8080 - galley: - host: galley - port: 8080 - gundeck: - host: gundeck - port: 8080 - federatorInternal: - host: federator - port: 8080 - spar: - host: spar - port: 8080 - rabbitmq: - host: rabbitmq - port: 5672 - vHost: / - adminPort: 15672 - enableTls: false - insecureSkipVerifyTls: false - # tlsCaSecretRef: - # name: - # key: - # Cassandra clusters used by background-worker - cassandra: - host: aws-cassandra - cassandraGalley: - host: aws-cassandra - cassandraBrig: - host: aws-cassandra - - # Postgres connection settings - # - # Values are described in https://www.postgresql.org/docs/17/libpq-connect.html#LIBPQ-PARAMKEYWORDS - # To set the password via a brig secret see `secrets.pgPassword`. - # - # `additionalVolumeMounts` and `additionalVolumes` can be used to mount - # additional files (e.g. certificates) into the brig container. This way - # does not work for password files (parameter `passfile`), because - # libpq-connect requires access rights (mask 0600) for them that we cannot - # provide for random uids. - # - # Below is an example configuration we're using for our CI tests. - postgresql: - host: postgresql # DNS name without protocol - port: "5432" - user: wire-server - dbname: wire-server - postgresqlPool: - size: 5 - acquisitionTimeout: 10s - agingTimeout: 1d - idlenessTimeout: 10m - - # Setting this to `true` will start conversation migration to postgresql. - # - # NOTE: It is very important that galley be configured to with - # `settings.postgresMigration.conversation` with `migration-to-postgresql` - # before setting this to `true`. - migrateConversations: false - migrateConversationsOptions: - pageSize: 10000 - parallelism: 2 - # This will start the migration of conversation codes. - # It's important to set `settings.postgresMigration.conversationCodes` to `migration-to-postgresql` - # before starting the migration. - migrateConversationCodes: false - # This will start the migration of team features. - # It's important to set `settings.postgresMigration.teamFeatures` to `migration-to-postgresql` - # before starting the migration. - migrateTeamFeatures: false - - backendNotificationPusher: - pushBackoffMinWait: 10000 # in microseconds, so 10ms - pushBackoffMaxWait: 300000000 # microseconds, so 300s - remotesRefreshInterval: 300000000 # microseconds, so 300s - - # Background jobs consumer configuration - backgroundJobs: - # Maximum number of in-flight jobs per process - concurrency: 8 - # Per-attempt timeout in seconds - jobTimeout: 60s - # Total attempts, including the first try - maxAttempts: 3 - - # Controls where conversation data is stored/accessed - postgresMigration: - conversation: cassandra - conversationCodes: cassandra - teamFeatures: cassandra - -secrets: - {} - # pgPassword: - -podSecurityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - runAsNonRoot: true - seccompProfile: - type: RuntimeDefault diff --git a/charts/brig/.helmignore b/charts/brig/.helmignore deleted file mode 100644 index f0c13194444..00000000000 --- a/charts/brig/.helmignore +++ /dev/null @@ -1,21 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*~ -# Various IDEs -.project -.idea/ -*.tmproj diff --git a/charts/brig/Chart.yaml b/charts/brig/Chart.yaml deleted file mode 100644 index 2bbd1a8ccad..00000000000 --- a/charts/brig/Chart.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -description: Brig (part of Wire Server) - User management -name: brig -version: 0.0.42 diff --git a/charts/brig/README.md b/charts/brig/README.md deleted file mode 100644 index 506b0cfee10..00000000000 --- a/charts/brig/README.md +++ /dev/null @@ -1,6 +0,0 @@ -Note that brig depends on some provisioned storage, namely: - -- cassandra -- elasticsearch-directory - -These are dealt with independently from this chart. diff --git a/charts/brig/templates/_helpers.tpl b/charts/brig/templates/_helpers.tpl deleted file mode 100644 index 2c3b801d674..00000000000 --- a/charts/brig/templates/_helpers.tpl +++ /dev/null @@ -1,66 +0,0 @@ - -{{/* Allow KubeVersion to be overridden. */}} -{{- define "kubeVersion" -}} - {{- default .Capabilities.KubeVersion.Version .Values.kubeVersionOverride -}} -{{- end -}} - -{{- define "includeSecurityContext" -}} - {{- (semverCompare ">= 1.24-0" (include "kubeVersion" .)) -}} -{{- end -}} - -{{- define "useCassandraTLS" -}} -{{ or (hasKey .cassandra "tlsCa") (hasKey .cassandra "tlsCaSecretRef") }} -{{- end -}} - -{{/* Return a Dict of TLS CA secret name and key -This is used to switch between provided secret (e.g. by cert-manager) and -created one (in case the CA is provided as PEM string.) -*/}} -{{- define "tlsSecretRef" -}} -{{- if .cassandra.tlsCaSecretRef -}} -{{ .cassandra.tlsCaSecretRef | toYaml }} -{{- else }} -{{- dict "name" "brig-cassandra" "key" "ca.pem" | toYaml -}} -{{- end -}} -{{- end -}} - - -{{- define "configureElasticSearchCa" -}} -{{ or (hasKey .elasticsearch "tlsCa") (hasKey .elasticsearch "tlsCaSecretRef") }} -{{- end -}} - -{{- define "elasticsearchTlsSecretName" -}} -{{- if .elasticsearch.tlsCaSecretRef -}} -{{ .elasticsearch.tlsCaSecretRef.name }} -{{- else }} -{{- print "brig-elasticsearch-ca" -}} -{{- end -}} -{{- end -}} - -{{- define "elasticsearchTlsSecretKey" -}} -{{- if .elasticsearch.tlsCaSecretRef -}} -{{ .elasticsearch.tlsCaSecretRef.key }} -{{- else }} -{{- print "ca.pem" -}} -{{- end -}} -{{- end -}} - -{{- define "configureAdditionalElasticSearchCa" -}} -{{ or (hasKey .elasticsearch "additionalTlsCa") (hasKey .elasticsearch "additionalTlsCaSecretRef") }} -{{- end -}} - -{{- define "additionalElasticsearchTlsSecretName" -}} -{{- if .elasticsearch.additionalTlsCaSecretRef -}} -{{ .elasticsearch.additionalTlsCaSecretRef.name }} -{{- else }} -{{- print "brig-additional-elasticsearch-ca" -}} -{{- end -}} -{{- end -}} - -{{- define "additionalElasticsearchTlsSecretKey" -}} -{{- if .elasticsearch.additionalTlsCaSecretRef -}} -{{ .elasticsearch.additionalTlsCaSecretRef.key }} -{{- else }} -{{- print "ca.pem" -}} -{{- end -}} -{{- end -}} diff --git a/charts/brig/templates/conf/_turn-servers-v2.txt.tpl b/charts/brig/templates/conf/_turn-servers-v2.txt.tpl deleted file mode 100644 index 0804b1f1729..00000000000 --- a/charts/brig/templates/conf/_turn-servers-v2.txt.tpl +++ /dev/null @@ -1,6 +0,0 @@ -{{ define "turn-servers-v2.txt" }} -{{- if eq $.Values.turn.serversSource "files" }} -{{ range .Values.turnStatic.v2 }}{{ . }} -{{ end -}} -{{- end }} -{{ end }} \ No newline at end of file diff --git a/charts/brig/templates/conf/_turn-servers.txt.tpl b/charts/brig/templates/conf/_turn-servers.txt.tpl deleted file mode 100644 index 36927b6c72f..00000000000 --- a/charts/brig/templates/conf/_turn-servers.txt.tpl +++ /dev/null @@ -1,6 +0,0 @@ -{{ define "turn-servers.txt" }} -{{- if eq $.Values.turn.serversSource "files" }} -{{ range .Values.turnStatic.v1 }}{{ . }} -{{ end -}} -{{- end }} -{{ end }} \ No newline at end of file diff --git a/charts/brig/values.yaml b/charts/brig/values.yaml deleted file mode 100644 index 76ccc8989b2..00000000000 --- a/charts/brig/values.yaml +++ /dev/null @@ -1,313 +0,0 @@ -replicaCount: 3 -image: - repository: quay.io/wire/brig - tag: do-not-use -service: - externalPort: 8080 - internalPort: 8080 -resources: - requests: - memory: "200Mi" - cpu: "100m" - limits: - memory: "512Mi" -metrics: - serviceMonitor: - enabled: false -livenessProbe: - # Maximum allowed network connections before forcing restart (mitigates connection/memory leaks) - maxConnections: 500 -# This is not supported for production use, only here for testing: -# preStop: -# exec: -# command: ["sh", "-c", "curl http://acme.example"] -config: - logLevel: Info - logFormat: StructuredJSON - logNetStrings: false - cassandra: - host: aws-cassandra - # To enable TLS provide a CA: - # tlsCa: - # - # Or refer to an existing secret (containing the CA): - # tlsCaSecretRef: - # name: - # key: - - elasticsearch: - scheme: http - host: elasticsearch-client - port: 9200 - index: directory - insecureSkipVerifyTls: false - # To configure custom TLS CA, please provide one of these: - # tlsCa: - # - # Or refer to an existing secret (containing the CA): - # tlsCaSecretRef: - # name: - # key: - additionalWriteScheme: http - # additionalWriteHost: - additionalWritePort: 9200 - # additionalWriteIndex: - additionalInsecureSkipVerifyTls: false - # To configure custom TLS CA, please provide one of these: - # additionalTlsCa: - # - # Or refer to an existing secret (containing the CA): - # additionalTlsCaSecretRef: - # name: - # key: - aws: - region: "eu-west-1" - sesEndpoint: https://email.eu-west-1.amazonaws.com - sqsEndpoint: https://sqs.eu-west-1.amazonaws.com - # dynamoDBEndpoint: https://dynamodb.eu-west-1.amazonaws.com - # -- If set to false, 'dynamoDBEndpoint' _must_ be set. - randomPrekeys: true - useSES: true - 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 - rabbitmq: - host: rabbitmq - port: 5672 - vHost: / - enableTls: false - insecureSkipVerifyTls: false - # tlsCaSecretRef: - # name: - # key: - - # Postgres connection settings - # - # Values are described in https://www.postgresql.org/docs/17/libpq-connect.html#LIBPQ-PARAMKEYWORDS - # To set the password via a brig secret see `secrets.pgPassword`. - # - # `additionalVolumeMounts` and `additionalVolumes` can be used to mount - # additional files (e.g. certificates) into the brig container. This way - # does not work for password files (parameter `passfile`), because - # libpq-connect requires access rights (mask 0600) for them that we cannot - # provide for random uids. - # - # Below is an example configuration we're using for our CI tests. - postgresql: - host: postgresql # DNS name without protocol - port: "5432" - user: wire-server - dbname: wire-server - postgresqlPool: - size: 100 - acquisitionTimeout: 10s - agingTimeout: 1d - idlenessTimeout: 10m - - emailSMS: - general: - templateBranding: - brand: Wire - brandUrl: https://wire.com - brandLabelUrl: wire.com - brandLogoUrl: https://wire.com/p/img/email/logo-email-black.png - brandService: Wire Service Provider - copyright: © WIRE SWISS GmbH - misuse: misuse@wire.com - legal: https://wire.com/legal/ - forgot: https://wire.com/forgot/ - support: https://support.wire.com/ - authSettings: - keyIndex: 1 - userTokenTimeout: 4838400 - sessionTokenTimeout: 86400 - accessTokenTimeout: 900 - providerTokenTimeout: 900 - legalholdUserTokenTimeout: 4838400 - legalholdAccessTokenTimeout: 900 - # sft: - # sftBaseDomain: sft.wire.example.com - # sftSRVServiceName: sft - # sftDiscoveryIntervalSeconds: 10 - # sftListLength: 20 - # sftToken: - # ttl: 120 - # secret: /etc/wire/brig/secrets/sftTokenSecret # this is the default path for secret.sftTokenSecret - optSettings: - setActivationTimeout: 1209600 - setTeamInvitationTimeout: 1814400 - setUserMaxConnections: 1000 - setCookieInsecure: false - setUserCookieRenewAge: 1209600 - setUserCookieLimit: 32 - setUserCookieThrottle: - stdDev: 3000 - retryAfter: 86400 - setRichInfoLimit: 5000 - setDefaultUserLocale: en - setMaxTeamSize: 10000 - setMaxConvSize: 500 - # Allowed values: https://github.com/wireapp/wire-server/blob/0126651a25aabc0c5589edc2b1988bb06550a03a/services/brig/src/Brig/Options.hs#L304-L306 - # Description: https://github.com/wireapp/wire-server/blob/0126651a25aabc0c5589edc2b1988bb06550a03a/services/brig/src/Brig/Options.hs#L290-L299 - setEmailVisibility: visible_to_self - setPropertyMaxKeyLen: 1024 - setPropertyMaxValueLen: 524288 - setDeleteThrottleMillis: 100 - # Allow search within same team only. Default: false - # setSearchSameTeamOnly: false|true - # Set max number of user clients. Default: 7 - # setUserMaxPermClients: - # Customer extensions. If this is not part of your contract with wire, use at your own risk! - # Details: https://github.com/wireapp/wire-server/blob/3d5684023c54fe580ab27c11d7dae8f19a29ddbc/services/brig/src/Brig/Options.hs#L465-L503 - # setCustomerExtensions: - # domainsBlockedForRegistration: - # - wire.example - set2FACodeGenerationDelaySecs: 300 # 5 minutes - setNonceTtlSecs: 300 # 5 minutes - setDpopMaxSkewSecs: 1 - setDpopTokenExpirationTimeSecs: 300 # 5 minutes - setOAuthAuthCodeExpirationTimeSecs: 300 # 5 minutes - setOAuthAccessTokenExpirationTimeSecs: 900 # 15 minutes - setOAuthRefreshTokenExpirationTimeSecs: 14515200 # 24 weeks - setOAuthEnabled: true - setOAuthMaxActiveRefreshTokens: 10 - # Disable one ore more API versions. Please make sure the configuration value is the same in all these charts: - # brig, cannon, cargohold, galley, gundeck, proxy, spar. - setDisabledAPIVersions: [development] - setFederationStrategy: allowNone - setFederationDomainConfigsUpdateFreq: 10 - setPasswordHashingOptions: - algorithm: scrypt # or argon2id - # When algorithm is argon2id, these can be configured: - # iterations: - # parallelism: - # memory: - setPasswordHashingRateLimit: - ipAddrLimit: - burst: 5 - inverseRate: 300000000 # 5 mins, makes it 12 reqs/hour - userLimit: - burst: 5 - inverseRate: 60000000 # 1 min, makes it 60 req/hour - internalLimit: - burst: 10 - inverseRate: 0 # No rate limiting for internal use - ipv4CidrBlock: 32 # Only block individual IP addresses - ipv6CidrBlock: 64 # Block /64 range at a time. - ipAddressExceptions: [] - maxRateLimitedKeys: 100000 # Estimated memory usage: 4 MB - # setAuditLogEmailRecipient: security@wire.com - setEphemeralUserCreationEnabled: true - - smtp: - passwordFile: /etc/wire/brig/secrets/smtp-password.txt - proxy: {} - wireServerEnterprise: - enabled: false - -turnStatic: - v1: - - turn:localhost:3478 - v2: - - turn:localhost:3478 - - turn:localhost:3478?transport=tcp - -turn: - serversSource: files # files | dns - # baseDomain: turn.wire.example # Must be configured if serversSource is dns - discoveryIntervalSeconds: 10 # Used only if serversSource is dns - -serviceAccount: - # When setting this to 'false', either make sure that a service account named - # 'brig' exists or change the 'name' field to 'default' - create: true - name: brig - annotations: {} - automountServiceAccountToken: true - -secrets: {} - -podSecurityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - runAsNonRoot: true - seccompProfile: - type: RuntimeDefault -tests: - config: - {} - # uploadXml: - # baseUrl: s3://bucket/path/ - - secrets: - # uploadXmlAwsAccessKeyId: - # uploadXmlAwsSecretAccessKey: - - # These "secrets" are only used in tests and are therefore safe to be stored unencrypted - providerPrivateKey: | - -----BEGIN RSA PRIVATE KEY----- - MIIEpAIBAAKCAQEAu+Kg/PHHU3atXrUbKnw0G06FliXcNt3lMwl2os5twEDcPPFw - /feGiAKymxp+7JqZDrseS5D9THGrW+OQRIPHWvUBdiLfGrZqJO223DB6D8K2Su/o - dmnjZJ2z23rhXoEArTplu+Dg9K+c2LVeXTKVVPOaOzgtAB21XKRiQ4ermqgi3/nj - r03rXyq/qNkuNd6tNcg+HAfGxfGvvCSYBfiSbUKr/BeArYRcjzr/h5m1In6fG/if - 9GEI6m8dxHT9JbY53wiksowy6ajCuqskIFg87X883H+LA/d6X5CTiPv1VMxXdBUi - GPuC9IT/6CNQ1/LFt0P37ax58+LGYlaFo7lanQIDAQABAoIBAQC0doVy7zgpLsBv - Sz0AnbPe1pjxEwRlntRbJSfSULySALqJvs5s4adSVGUBHX3z/LousAP1SRpCppuU - 8wrLBFgjQVlaAzyQB84EEl+lNtrG8Jrvd2es9R/4sJDkqy50+yuPN5wnzWPFIjhg - 3jP5CHDu29y0LMzsY5yjkzDe9B0bueXEZVU+guRjhpwHHKOFeAr9J9bugFUwgeAr - jF0TztzFAb0fsUNPiQAho1J5PyjSVgItaPfAPv/p30ROG+rz+Rd5NSSvBC5F+yOo - azb84zzwCg/knAfIz7SOMRrmBh2qhGZFZ8gXdq65UaYv+cpT/qo28mpAT2vOkyeD - aPZp0ysBAoGBAOQROoDipe/5BTHBcXYuUE1qa4RIj3wgql5I8igXr4K6ppYBmaOg - DL2rrnqD86chv0P4l/XOomKFwYhVGXtqRkeYnk6mQXwNVkgqcGbY5PSNyMg5+ekq - jSOOPHGzzTWKzYuUDUpB/Lf6jbTv8fq2GYW3ZYiqQ/xiugOvglZrTE7NAoGBANLl - irjByfxAWGhzCrDx0x5MBpsetadI9wUA8u1BDdymsRg73FDn3z7NipVUAMDXMGVj - lqbCRlHESO2yP4GaPEA4FM+MbTZSuhAYV+SY07mEPLHF64/nJas83Zp91r5rhaqJ - L9rWCl3KJ5OUnr3YizCnHIW72FxjwtpjxHJLupsRAoGAGIbhy8qUHeKh9F/hW9xP - NoQjW+6Rv7+jktA1eqpRbbW1BJzXcQldVWiJMxPNuEOg1iZ98SlvvTi1P3wnaWZc - eIapP7wRfs3QYaJuxCC/Pq2g0ieqALFazGAXkALOJtvujvw1Ea9XBlIjuzmyxEuh - Iwg+Gxx0g0f6yTquwax4YGECgYEAnpAK3qKFNO1ECzQDo8oNy0ep59MNDPtlDhQK - katJus5xdCD9oq7TQKrVOTTxZAvmzTQ1PqfuqueDVYOhD9Zg2n/P1cRlEGTek99Z - pfvppB/yak6+r3FA9yBKFS/r1zuMQg3nNweav62QV/tz5pT7AdeDMGFtaPlwtTYx - qyWY5aECgYBPySbPccNj+xxQzxcti2y/UXjC04RgOA/Hm1D0exa0vBqS9uxlOdG8 - F47rKenpBrslvdfTVsCDB1xyP2ebWVzp6EqMycw6OLPxgo3fBfZ4pi6P+rByh0Cc - Lhfh+ET0CPnKCxtop3lUrn4ZvqchS0j3J+M0pDuqoWF5hfKxFhkEIw== - -----END RSA PRIVATE KEY----- - providerPublicKey: | - -----BEGIN PUBLIC KEY----- - MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu+Kg/PHHU3atXrUbKnw0 - G06FliXcNt3lMwl2os5twEDcPPFw/feGiAKymxp+7JqZDrseS5D9THGrW+OQRIPH - WvUBdiLfGrZqJO223DB6D8K2Su/odmnjZJ2z23rhXoEArTplu+Dg9K+c2LVeXTKV - VPOaOzgtAB21XKRiQ4ermqgi3/njr03rXyq/qNkuNd6tNcg+HAfGxfGvvCSYBfiS - bUKr/BeArYRcjzr/h5m1In6fG/if9GEI6m8dxHT9JbY53wiksowy6ajCuqskIFg8 - 7X883H+LA/d6X5CTiPv1VMxXdBUiGPuC9IT/6CNQ1/LFt0P37ax58+LGYlaFo7la - nQIDAQAB - -----END PUBLIC KEY----- - providerPublicCert: | - -----BEGIN CERTIFICATE----- - MIIDdjCCAl4CCQCm0AiwERR/qjANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJE - RTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xGDAWBgNVBAoMD1dp - cmUgU3dpc3MgR21iSDERMA8GA1UEAwwId2lyZS5jb20xHzAdBgkqhkiG9w0BCQEW - EGJhY2tlbmRAd2lyZS5jb20wHhcNMTYwODA0MTMxNDQyWhcNMzYwNzMwMTMxNDQy - WjB9MQswCQYDVQQGEwJERTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJs - aW4xGDAWBgNVBAoMD1dpcmUgU3dpc3MgR21iSDERMA8GA1UEAwwId2lyZS5jb20x - HzAdBgkqhkiG9w0BCQEWEGJhY2tlbmRAd2lyZS5jb20wggEiMA0GCSqGSIb3DQEB - AQUAA4IBDwAwggEKAoIBAQC74qD88cdTdq1etRsqfDQbToWWJdw23eUzCXaizm3A - QNw88XD994aIArKbGn7smpkOux5LkP1Mcatb45BEg8da9QF2It8atmok7bbcMHoP - wrZK7+h2aeNknbPbeuFegQCtOmW74OD0r5zYtV5dMpVU85o7OC0AHbVcpGJDh6ua - qCLf+eOvTetfKr+o2S413q01yD4cB8bF8a+8JJgF+JJtQqv8F4CthFyPOv+HmbUi - fp8b+J/0YQjqbx3EdP0ltjnfCKSyjDLpqMK6qyQgWDztfzzcf4sD93pfkJOI+/VU - zFd0FSIY+4L0hP/oI1DX8sW3Q/ftrHnz4sZiVoWjuVqdAgMBAAEwDQYJKoZIhvcN - AQELBQADggEBAEuwlHElIGR56KVC1dJiw238mDGjMfQzSP76Wi4zWS6/zZwJUuog - BkC+vacfju8UAMvL+vdqkjOVUHor84/2wuq0qn91AjOITD7tRAZB+XLXxsikKv/v - OXE3A/lCiNi882NegPyXAfFPp/71CIiTQZps1eQkAvhD5t5WiFYPESxDlvEJrHFY - XP4+pp8fL8YPS7iZNIq+z+P8yVIw+B/Hs0ht7wFIYN0xACbU8m9+Rs08JMoT16c+ - hZMuK3BWD3fzkQVfW0yMwz6fWRXB483ZmekGkgndOTDoJQMdJXZxHpI3t2FcxQYj - T45GXxRd18neXtuYa/OoAw9UQFDN5XfXN0g= - -----END CERTIFICATE----- - - # pgPassword: -test: - elasticsearch: - additionalHost: elasticsearch-ephemeral diff --git a/charts/cannon/.helmignore b/charts/cannon/.helmignore deleted file mode 100644 index f0c13194444..00000000000 --- a/charts/cannon/.helmignore +++ /dev/null @@ -1,21 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*~ -# Various IDEs -.project -.idea/ -*.tmproj diff --git a/charts/cannon/Chart.yaml b/charts/cannon/Chart.yaml deleted file mode 100644 index 1a30687605b..00000000000 --- a/charts/cannon/Chart.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -description: A Helm chart for cannon in Kubernetes -name: cannon -version: 0.0.42 diff --git a/charts/cannon/templates/_helpers.tpl b/charts/cannon/templates/_helpers.tpl deleted file mode 100644 index d6cc652620b..00000000000 --- a/charts/cannon/templates/_helpers.tpl +++ /dev/null @@ -1,41 +0,0 @@ -{{/* vim: set filetype=mustache: */}} -{{/* -Expand the name of the chart. -*/}} -{{- define "cannon.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} -{{- end -}} - -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -*/}} -{{- define "cannon.fullname" -}} -{{- $name := default .Chart.Name .Values.nameOverride -}} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} -{{- end -}} - -{{/* Allow KubeVersion to be overridden. */}} -{{- define "kubeVersion" -}} - {{- default .Capabilities.KubeVersion.Version .Values.kubeVersionOverride -}} -{{- end -}} - -{{- define "includeSecurityContext" -}} - {{- (semverCompare ">= 1.24-0" (include "kubeVersion" .)) -}} -{{- end -}} - -{{- define "useCassandraTLS" -}} -{{ or (hasKey .cassandra "tlsCa") (hasKey .cassandra "tlsCaSecretRef") }} -{{- end -}} - -{{/* Return a Dict of TLS CA secret name and key -This is used to switch between provided secret (e.g. by cert-manager) and -created one (in case the CA is provided as PEM string.) -*/}} -{{- define "tlsSecretRef" -}} -{{- if .cassandra.tlsCaSecretRef -}} -{{ .cassandra.tlsCaSecretRef | toYaml }} -{{- else }} -{{- dict "name" "cannon-cassandra" "key" "ca.pem" | toYaml -}} -{{- end -}} -{{- end -}} diff --git a/charts/cannon/templates/nginz-certificate-secret.yaml b/charts/cannon/templates/nginz-certificate-secret.yaml deleted file mode 100644 index 05e552ce949..00000000000 --- a/charts/cannon/templates/nginz-certificate-secret.yaml +++ /dev/null @@ -1,15 +0,0 @@ -{{- if and .Values.service.nginz.enabled (not .Values.service.nginz.certManager.enabled ) }} -apiVersion: v1 -kind: Secret -metadata: - name: {{ .Values.service.nginz.tls.secretName }} - labels: - app: cannon-nginz - chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" - release: "{{ .Release.Name }}" - heritage: "{{ .Release.Service }}" -type: kubernetes.io/tls -data: - tls.crt: {{ .Values.secrets.nginz.tls.crt | b64enc | quote }} - tls.key: {{ .Values.secrets.nginz.tls.key | b64enc | quote }} -{{- end }} diff --git a/charts/cannon/values.yaml b/charts/cannon/values.yaml deleted file mode 100644 index 34296ca5bb0..00000000000 --- a/charts/cannon/values.yaml +++ /dev/null @@ -1,185 +0,0 @@ -replicaCount: 3 -image: - repository: quay.io/wire/cannon - tag: do-not-use - pullPolicy: IfNotPresent -# Optional extra arguments passed to cannon (e.g. ["+RTS", "-M2g", "-RTS"]) -cannonArgs: [] -nginzImage: - repository: quay.io/wire/nginz - tag: do-not-use - pullPolicy: IfNotPresent -config: - logLevel: Info - logFormat: StructuredJSON - logNetStrings: false - rabbitmq: - host: rabbitmq - port: 5672 - vHost: / - enableTls: false - insecureSkipVerifyTls: false - rabbitMqMaxConnections: 1000 - rabbitMqMaxChannels: 300 - cassandra: - host: aws-cassandra - # To enable TLS provide a CA: - # tlsCa: - # - # Or refer to an existing secret (containing the CA): - # tlsCaSecretRef: - # name: - # key: - - # See also the section 'Controlling the speed of websocket draining during - # cannon pod replacement' in docs/how-to/install/configuration-options.rst - drainOpts: - # The following drains a minimum of 400 connections/second - # for a total of 10000 over 25 seconds - # (if cannon holds more connections, draining will happen at a faster pace) - gracePeriodSeconds: 25 - millisecondsBetweenBatches: 50 - minBatchSize: 20 - - # TTL of stored notifications in Seconds. After this period, notifications - # will be deleted and thus not delivered. - # The default is 28 days. - notificationTTL: 2419200 - - # Disable one ore more API versions. Please make sure the configuration value is the same in all these charts: - # brig, cannon, cargohold, galley, gundeck, proxy, spar. - disabledAPIVersions: [ development ] - -metrics: - serviceMonitor: - enabled: false - -nginx_conf: - zauth_keystore: /etc/wire/nginz/secrets/zauth.conf - zauth_acl: /etc/wire/nginz/conf/zauth.acl - worker_processes: auto - worker_rlimit_nofile: 131072 - worker_connections: 65536 - disabled_paths: [] - rate_limit_reqs_per_user: "10r/s" - rate_limit_reqs_per_addr: "5r/m" - user_rate_limit_request_zones: [] - - tls: - protocols: TLSv1.2 TLSv1.3 - # NOTE: These are some sane defaults (compliant to TR-02102-2), you may want to overrride them on your own installation - # For TR-02102-2 see https://www.bsi.bund.de/SharedDocs/Downloads/EN/BSI/Publications/TechGuidelines/TG02102/BSI-TR-02102-2.html - # As a Wire employee, for Wire-internal discussions and context see - # * https://wearezeta.atlassian.net/browse/FS-33 - # * https://wearezeta.atlassian.net/browse/FS-444 - ciphers_tls12: "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384" - ciphers_tls13: "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384" - - # The origins from which we allow CORS requests. These are combined with - # 'external_env_domain' and 'additional_external_env_domains' to form a full - # url. - allowlisted_origins: - - webapp - - teams - - account - - # The origins from which we allow CORS requests at random ports. This is - # useful for testing with HTTP proxies and should not be used in production. - # The list entries must be full hostnames (they are **not** combined with - # 'external_env_domain'). http and https URLs are allow listed. - randomport_allowlisted_origins: [] # default is empty by intention - - # Configure multiple root domains for one backend. This is only advised in - # very specicial cases. Usually, this list should be empty. - additional_external_env_domains: [] - - # Setting this value does nothing as the only upstream recongnized here is - # 'cannon' and is forwarded to localhost. This is here only to make sure that - # nginx.conf templating doesn't differ too much with the one in nginz helm - # chart. - upstream_namespace: {} - - # Only upstream recognized by the generated nginx config is 'cannon', the - # server for this will be cannon running on localhost. This setting is like - # this so that templating for nginx.conf doesn't differ too much from the one - # in the nginz helm chart. - upstreams: - cannon: - - path: /await - envs: - - all - use_websockets: true - - path: /websocket - envs: - - all - use_websockets: true - - path: /events - envs: - - all - use_websockets: true - -# FUTUREWORK: allow resources for cannon and nginz to be different -resources: - requests: - memory: "256Mi" - cpu: "100m" - limits: - memory: "1024Mi" -service: - name: cannon - internalPort: 8080 - externalPort: 8080 - nginz: - # Enable this only if service of `type: LoadBalancer` can work in your K8s - # cluster. - enabled: false - # hostname: # Needed when using either externalDNS or certManager - name: cannon-nginz - internalPort: 8443 - externalPort: 443 - annotations: {} - tls: - secretName: cannon-nginz-cert - externalDNS: - enabled: false - ttl: "10m" - certManager: - # When certManager is not enabled, certificates must be provided at - # .secrets.nginz.tls.crt and .secrets.nginz.tls.key. - enabled: false - certificate: - name: cannon-nginz - issuer: - name: letsencrypt - kind: ClusterIssuer - -podSecurityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - runAsNonRoot: true - seccompProfile: - type: RuntimeDefault - -# nodeSelector: -# wire.com/role: cannon -nodeSelector: {} - -# affinity: -# nodeAffinity: -# requiredDuringSchedulingIgnoredDuringExecution: -# nodeSelectorTerms: -# - matchExpressions: -# - key: wire.com/role -# operator: In -# values: -# - cannon -affinity: {} - -# tolerations: -# - key: "wire.com/role" -# operator: "Equal" -# value: "cannon" -# effect: "NoSchedule" -tolerations: [] diff --git a/charts/cargohold/.helmignore b/charts/cargohold/.helmignore deleted file mode 100644 index f0c13194444..00000000000 --- a/charts/cargohold/.helmignore +++ /dev/null @@ -1,21 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*~ -# Various IDEs -.project -.idea/ -*.tmproj diff --git a/charts/cargohold/Chart.yaml b/charts/cargohold/Chart.yaml deleted file mode 100644 index fa2a8150915..00000000000 --- a/charts/cargohold/Chart.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -description: Cargohold (part of Wire Server) - Asset storage -name: cargohold -version: 0.0.42 diff --git a/charts/cargohold/templates/_helpers.tpl b/charts/cargohold/templates/_helpers.tpl deleted file mode 100644 index 762fb52c2fa..00000000000 --- a/charts/cargohold/templates/_helpers.tpl +++ /dev/null @@ -1,9 +0,0 @@ - -{{/* Allow KubeVersion to be overridden. */}} -{{- define "kubeVersion" -}} - {{- default .Capabilities.KubeVersion.Version .Values.kubeVersionOverride -}} -{{- end -}} - -{{- define "includeSecurityContext" -}} - {{- (semverCompare ">= 1.24-0" (include "kubeVersion" .)) -}} -{{- end -}} diff --git a/charts/cargohold/values.yaml b/charts/cargohold/values.yaml deleted file mode 100644 index 79a34eb279f..00000000000 --- a/charts/cargohold/values.yaml +++ /dev/null @@ -1,64 +0,0 @@ -replicaCount: 3 -image: - repository: quay.io/wire/cargohold - tag: do-not-use -service: - externalPort: 8080 - internalPort: 8080 -metrics: - serviceMonitor: - enabled: false -resources: - requests: - memory: "80Mi" - cpu: "100m" - limits: - memory: "200Mi" -config: - logLevel: Info - logFormat: StructuredJSON - logNetStrings: false - enableFederation: false # keep in sync with background-worker, brig and galley charts' config.enableFederation as well as wire-server chart's tags.federation - aws: - region: "eu-west-1" - s3Bucket: assets - # Multi-ingress configuration: - # multiIngress: - # - nginz-https.red.wire.example: assets.red.wire.example - # - nginz-https.green.wire.example: assets.green.wire.example - proxy: {} - settings: - maxTotalBytes: 104857632 # limit to 100 MiB + 32 bytes - maxTotalBytesStrict: 26214432 # limit to 25 MiB + 32 bytes - downloadLinkTTL: 300 # Seconds - assetAuditLogEnabled: false - # Disable one ore more API versions. Please make sure the configuration value is the same in all these charts: - # brig, cannon, cargohold, galley, gundeck, proxy, spar. - disabledAPIVersions: [ development ] - -serviceAccount: - # When setting this to 'false', either make sure that a service account named - # 'cargohold' exists or change the 'name' field to 'default' - create: true - name: cargohold - annotations: {} - automountServiceAccountToken: true - -secrets: {} - -podSecurityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - runAsNonRoot: true - seccompProfile: - type: RuntimeDefault -tests: - config: {} -# config: -# uploadXml: -# baseUrl: s3://bucket/path/ -# secrets: -# uploadXmlAwsAccessKeyId: -# uploadXmlAwsSecretAccessKey: diff --git a/charts/elasticsearch-index/templates/migrate-data.yaml b/charts/elasticsearch-index/templates/migrate-data.yaml index 3bf41e02a61..0b6ded659ab 100644 --- a/charts/elasticsearch-index/templates/migrate-data.yaml +++ b/charts/elasticsearch-index/templates/migrate-data.yaml @@ -57,6 +57,20 @@ spec: {{- if .Values.elasticsearch.insecureSkipTlsVerify }} - --elasticsearch-insecure-skip-tls-verify {{- end }} + - --pg-pool-size + - {{ .Values.postgresqlPool.size | quote }} + - --pg-pool-acquisition-timeout + - {{ .Values.postgresqlPool.acquisitionTimeout | quote }} + - --pg-pool-aging-timeout + - {{ .Values.postgresqlPool.agingTimeout | quote }} + - --pg-pool-idleness-timeout + - {{ .Values.postgresqlPool.idlenessTimeout | quote }} + {{- if hasKey $.Values.secrets "pgPassword" }} + - --pg-password-file + - /etc/wire/elasticsearch-index/secrets/pgPassword + {{- end }} + - --pg-settings + - {{ toJson .Values.postgresql | quote }} volumeMounts: {{- if hasKey .Values.secrets "elasticsearch" }} - name: "elasticsearch-index-secrets" @@ -70,6 +84,9 @@ spec: - name: elasticsearch-ca mountPath: "/certs/elasticsearch" {{- end }} + {{- if .Values.migrateData.additionalVolumeMounts }} + {{ toYaml .Values.migrateData.additionalVolumeMounts | nindent 10 }} + {{- end }} volumes: {{- if hasKey .Values.secrets "elasticsearch" }} - name: elasticsearch-index-secrets @@ -86,3 +103,6 @@ spec: secret: secretName: {{ include "elasticsearchTlsSecretName" .Values }} {{- end }} + {{- if .Values.migrateData.additionalVolumes }} + {{ toYaml .Values.migrateData.additionalVolumes | nindent 8 }} + {{- end }} diff --git a/charts/elasticsearch-index/templates/secret.yaml b/charts/elasticsearch-index/templates/secret.yaml index cda93a046bc..a88a8f6a8bc 100644 --- a/charts/elasticsearch-index/templates/secret.yaml +++ b/charts/elasticsearch-index/templates/secret.yaml @@ -16,5 +16,8 @@ data: {{- if .elasticsearch }} elasticsearch-credentials.yaml: {{ .elasticsearch | toYaml | b64enc }} {{- end }} + {{- if .pgPassword }} + pgPassword: {{ .pgPassword | b64enc | quote }} + {{- end }} {{- end }} {{- end }} diff --git a/charts/elasticsearch-index/values.yaml b/charts/elasticsearch-index/values.yaml index a7c136f233f..b653042eeac 100644 --- a/charts/elasticsearch-index/values.yaml +++ b/charts/elasticsearch-index/values.yaml @@ -13,20 +13,46 @@ elasticsearch: # name: # key: insecureSkipTlsVerify: false + cassandra: # host: port: 9042 keyspace: brig -# To enable TLS provide a CA: -# tlsCa: + # To enable TLS provide a CA: + # tlsCa: + # + # Or refer to an existing secret (containing the CA): + # tlsCaSecretRef: + # name: + # key: + +# Postgres connection settings # -# Or refer to an existing secret (containing the CA): -# tlsCaSecretRef: -# name: -# key: +# Values are described in https://www.postgresql.org/docs/17/libpq-connect.html#LIBPQ-PARAMKEYWORDS +# To set the password via a brig secret see `secrets.pgPassword`. +# +# `additionalVolumeMounts` and `additionalVolumes` under `migrateData` can be used to mount +# additional files (e.g. certificates) into the brig-index-migrate-data container. This way +# does not work for password files (parameter `passfile`), because +# libpq-connect requires access rights (mask 0600) for them that we cannot +# provide for random uids. +# +# Below is an example configuration we're using for our CI tests. +postgresql: + host: postgresql # DNS name without protocol + port: "5432" + user: wire-server + dbname: wire-server +postgresqlPool: + size: 100 + acquisitionTimeout: 10s + agingTimeout: 1d + idlenessTimeout: 10m + galley: host: galley port: 8080 + image: repository: quay.io/wire/brig-index tag: do-not-use @@ -40,4 +66,8 @@ podSecurityContext: seccompProfile: type: RuntimeDefault +migrateData: + additionalVolumes: [] + additionalVolumeMounts: [] + secrets: {} diff --git a/charts/galley/Chart.yaml b/charts/galley/Chart.yaml deleted file mode 100644 index 916f06ed063..00000000000 --- a/charts/galley/Chart.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -description: Galley (part of Wire Server) - Conversations -name: galley -version: 0.0.42 diff --git a/charts/galley/templates/_helpers.tpl b/charts/galley/templates/_helpers.tpl deleted file mode 100644 index a9de4a20a9b..00000000000 --- a/charts/galley/templates/_helpers.tpl +++ /dev/null @@ -1,25 +0,0 @@ - -{{/* Allow KubeVersion to be overridden. */}} -{{- define "kubeVersion" -}} - {{- default .Capabilities.KubeVersion.Version .Values.kubeVersionOverride -}} -{{- end -}} - -{{- define "includeSecurityContext" -}} - {{- (semverCompare ">= 1.24-0" (include "kubeVersion" .)) -}} -{{- end -}} - -{{- define "useCassandraTLS" -}} -{{ or (hasKey .cassandra "tlsCa") (hasKey .cassandra "tlsCaSecretRef") }} -{{- end -}} - -{{/* Return a Dict of TLS CA secret name and key -This is used to switch between provided secret (e.g. by cert-manager) and -created one (in case the CA is provided as PEM string.) -*/}} -{{- define "tlsSecretRef" -}} -{{- if .cassandra.tlsCaSecretRef -}} -{{ .cassandra.tlsCaSecretRef | toYaml }} -{{- else }} -{{- dict "name" "galley-cassandra" "key" "ca.pem" | toYaml -}} -{{- end -}} -{{- end -}} diff --git a/charts/galley/templates/secret.yaml b/charts/galley/templates/secret.yaml deleted file mode 100644 index 69723581a4c..00000000000 --- a/charts/galley/templates/secret.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: galley - labels: - app: galley - chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" - release: "{{ .Release.Name }}" - heritage: "{{ .Release.Service }}" -type: Opaque -data: - {{- if .Values.secrets.mlsPrivateKeys }} - removal_ed25519.pem: {{ .Values.secrets.mlsPrivateKeys.removal.ed25519 | b64enc | quote }} - removal_ecdsa_secp256r1_sha256.pem: {{ .Values.secrets.mlsPrivateKeys.removal.ecdsa_secp256r1_sha256 | b64enc | quote }} - removal_ecdsa_secp384r1_sha384.pem: {{ .Values.secrets.mlsPrivateKeys.removal.ecdsa_secp384r1_sha384 | b64enc | quote }} - removal_ecdsa_secp521r1_sha512.pem: {{ .Values.secrets.mlsPrivateKeys.removal.ecdsa_secp521r1_sha512 | b64enc | quote }} - {{- end -}} - - {{- if $.Values.config.enableFederation }} - rabbitmqUsername: {{ .Values.secrets.rabbitmq.username | b64enc | quote }} - rabbitmqPassword: {{ .Values.secrets.rabbitmq.password | b64enc | quote }} - {{- end }} - - {{- if .Values.secrets.pgPassword }} - pgPassword: {{ .Values.secrets.pgPassword | b64enc | quote }} - {{- end }} diff --git a/charts/galley/values.yaml b/charts/galley/values.yaml deleted file mode 100644 index dd0517feef5..00000000000 --- a/charts/galley/values.yaml +++ /dev/null @@ -1,395 +0,0 @@ -replicaCount: 3 -image: - repository: quay.io/wire/galley - tag: do-not-use - schemaRepository: quay.io/wire/galley-schema -service: - externalPort: 8080 - internalPort: 8080 -metrics: - serviceMonitor: - enabled: false -resources: - requests: - memory: "100Mi" - cpu: "100m" - limits: - memory: "500Mi" -# This is not supported for production use, only here for testing: -# preStop: -# exec: -# command: ["sh", "-c", "curl http://acme.example"] -config: - logLevel: Info - logFormat: StructuredJSON - logNetStrings: false - cassandra: - host: aws-cassandra - replicaCount: 3 - # To enable TLS provide a CA: - # tlsCa: - # - # Or refer to an existing secret (containing the CA): - # tlsCaSecretRef: - # name: - # key: - - # Postgres connection settings - # - # Values are described in https://www.postgresql.org/docs/17/libpq-connect.html#LIBPQ-PARAMKEYWORDS - # To set the password via a brig secret see `secrets.pgPassword`. - # - # `additionalVolumeMounts` and `additionalVolumes` can be used to mount - # additional files (e.g. certificates) into the galley container. This way - # does not work for password files (parameter `passfile`), because - # libpq-connect requires access rights (mask 0600) for them that we cannot - # provide for random uids. - # - # Below is an example configuration we're using for our CI tests. - postgresql: - host: postgresql # DNS name without protocol - port: "5432" - user: wire-server - dbname: wire-server - postgresqlPool: - size: 100 - acquisitionTimeout: 10s - agingTimeout: 1d - idlenessTimeout: 10m - - enableFederation: false # keep in sync with background-worker, brig and cargohold charts' config.enableFederation as well as wire-server chart's tags.federation - # Not used if enableFederation is false - rabbitmq: - host: rabbitmq - port: 5672 - vHost: / - enableTls: false - insecureSkipVerifyTls: false - # tlsCaSecretRef: - # name: - # key: - - postgresMigration: - conversation: cassandra - conversationCodes: cassandra - teamFeatures: cassandra - settings: - httpPoolSize: 128 - maxTeamSize: 10000 - exposeInvitationURLsTeamAllowlist: [] - maxConvSize: 500 - intraListing: true - # Either `conversationCodeURI` or `multiIngress` must be set - # - # `conversationCodeURI` is the URI prefix for conversation invitation links - # It should be of form https://{ACCOUNT_PAGES}/conversation-join/ - conversationCodeURI: null - # - # `multiIngress` is a `Z-Host` depended setting of conversationCodeURI. - # Use this only if you want to expose the instance on multiple ingresses. - # If set it must a map from `Z-Host` to URI prefix - # Example: - # multiIngress: - # wire.example: https://accounts.wire.example/conversation-join/ - # example.net: https://accounts.example.net/conversation-join/ - multiIngress: null - # Disable one ore more API versions. Please make sure the configuration value is the same in all these charts: - # brig, cannon, cargohold, galley, gundeck, proxy, spar. - disabledAPIVersions: [development] - # The lifetime of a conversation guest link in seconds. Must be a value 0 < x <= 31536000 (365 days) - # Default is 31536000 (365 days) if not set - guestLinkTTLSeconds: 31536000 - passwordHashingOptions: - algorithm: scrypt # or argon2id - # When algorithm is argon2id, these can be configured: - # iterations: - # parallelism: - # memory: - passwordHashingRateLimit: - ipAddrLimit: - burst: 5 - inverseRate: 300000000 # 5 mins, makes it 12 reqs/hour - userLimit: - burst: 5 - inverseRate: 60000000 # 1 min, makes it 60 req/hour - internalLimit: - burst: 10 - inverseRate: 0 # No rate limiting for internal use - ipv4CidrBlock: 32 # Only block individual IP addresses - ipv6CidrBlock: 64 # Block /64 range at a time. - ipAddressExceptions: [] - maxRateLimitedKeys: 100000 # Estimated memory usage: 4 MB - - checkGroupInfo: false - - meetings: - validityPeriod: "48h" - - # To disable proteus for new federated conversations: - # federationProtocols: ["mls"] - - featureFlags: # see #RefConfigOptions in `/docs/reference` (https://github.com/wireapp/wire-server/) - appLock: - defaults: - config: - enforceAppLock: false - inactivityTimeoutSecs: 60 - status: enabled - classifiedDomains: - config: - domains: [] - status: disabled - conferenceCalling: - defaults: - status: enabled - lockStatus: locked - conversationGuestLinks: - defaults: - lockStatus: unlocked - status: enabled - fileSharing: - defaults: - lockStatus: unlocked - status: enabled - enforceFileDownloadLocation: - defaults: - lockStatus: locked - status: disabled - config: {} - legalhold: disabled-by-default - mls: - defaults: - status: disabled - config: - protocolToggleUsers: [] - defaultProtocol: proteus - allowedCipherSuites: [2] - defaultCipherSuite: 2 - supportedProtocols: [proteus, mls] # must contain defaultProtocol - groupInfoDiagnostics: false - lockStatus: unlocked - searchVisibilityInbound: - defaults: - status: disabled - selfDeletingMessages: - defaults: - config: - enforcedTimeoutSeconds: 0 - lockStatus: unlocked - status: enabled - sndFactorPasswordChallenge: - defaults: - lockStatus: locked - status: disabled - sso: disabled-by-default - teamSearchVisibility: disabled-by-default - validateSAMLEmails: - defaults: - status: enabled - outlookCalIntegration: - defaults: - status: disabled - lockStatus: locked - mlsE2EId: - defaults: - status: disabled - config: - verificationExpiration: 86400 - acmeDiscoveryUrl: null - lockStatus: unlocked - mlsMigration: - defaults: - status: disabled - config: - startTime: null # "2029-05-16T10:11:12.123Z" - finaliseRegardlessAfter: null # "2029-10-17T00:00:00.000Z" - usersThreshold: 100 - clientsThreshold: 100 - lockStatus: locked - limitedEventFanout: - defaults: - status: disabled - domainRegistration: - defaults: - status: disabled - lockStatus: locked - channels: - defaults: - status: disabled - 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: - mlsConversationReset: false - assetAuditLog: - status: disabled - consumableNotifications: - defaults: - status: disabled - lockStatus: locked - chatBubbles: - defaults: - status: disabled - lockStatus: locked - apps: - defaults: - status: disabled - lockStatus: locked - simplifiedUserConnectionRequestQRCode: - defaults: - status: enabled - lockStatus: unlocked - stealthUsers: - defaults: - status: disabled - lockStatus: locked - meetings: - defaults: - status: enabled - lockStatus: unlocked - meetingsPremium: - defaults: - status: disabled - lockStatus: locked - aws: - region: "eu-west-1" - proxy: {} -serviceAccount: - # When setting this to 'false', either make sure that a service account named - # 'galley' exists or change the 'name' field to 'default' - create: true - name: galley - annotations: {} - automountServiceAccountToken: true - -secrets: {} - -podSecurityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - runAsNonRoot: true - seccompProfile: - type: RuntimeDefault -tests: - config: - {} - # uploadXml: - # baseUrl: s3://bucket/path/ - - secrets: - # uploadXmlAwsAccessKeyId: - # uploadXmlAwsSecretAccessKey: - - # These "secrets" are only used in tests and are therefore safe to be stored unencrypted - providerPrivateKey: | - -----BEGIN RSA PRIVATE KEY----- - MIIEpAIBAAKCAQEAu+Kg/PHHU3atXrUbKnw0G06FliXcNt3lMwl2os5twEDcPPFw - /feGiAKymxp+7JqZDrseS5D9THGrW+OQRIPHWvUBdiLfGrZqJO223DB6D8K2Su/o - dmnjZJ2z23rhXoEArTplu+Dg9K+c2LVeXTKVVPOaOzgtAB21XKRiQ4ermqgi3/nj - r03rXyq/qNkuNd6tNcg+HAfGxfGvvCSYBfiSbUKr/BeArYRcjzr/h5m1In6fG/if - 9GEI6m8dxHT9JbY53wiksowy6ajCuqskIFg87X883H+LA/d6X5CTiPv1VMxXdBUi - GPuC9IT/6CNQ1/LFt0P37ax58+LGYlaFo7lanQIDAQABAoIBAQC0doVy7zgpLsBv - Sz0AnbPe1pjxEwRlntRbJSfSULySALqJvs5s4adSVGUBHX3z/LousAP1SRpCppuU - 8wrLBFgjQVlaAzyQB84EEl+lNtrG8Jrvd2es9R/4sJDkqy50+yuPN5wnzWPFIjhg - 3jP5CHDu29y0LMzsY5yjkzDe9B0bueXEZVU+guRjhpwHHKOFeAr9J9bugFUwgeAr - jF0TztzFAb0fsUNPiQAho1J5PyjSVgItaPfAPv/p30ROG+rz+Rd5NSSvBC5F+yOo - azb84zzwCg/knAfIz7SOMRrmBh2qhGZFZ8gXdq65UaYv+cpT/qo28mpAT2vOkyeD - aPZp0ysBAoGBAOQROoDipe/5BTHBcXYuUE1qa4RIj3wgql5I8igXr4K6ppYBmaOg - DL2rrnqD86chv0P4l/XOomKFwYhVGXtqRkeYnk6mQXwNVkgqcGbY5PSNyMg5+ekq - jSOOPHGzzTWKzYuUDUpB/Lf6jbTv8fq2GYW3ZYiqQ/xiugOvglZrTE7NAoGBANLl - irjByfxAWGhzCrDx0x5MBpsetadI9wUA8u1BDdymsRg73FDn3z7NipVUAMDXMGVj - lqbCRlHESO2yP4GaPEA4FM+MbTZSuhAYV+SY07mEPLHF64/nJas83Zp91r5rhaqJ - L9rWCl3KJ5OUnr3YizCnHIW72FxjwtpjxHJLupsRAoGAGIbhy8qUHeKh9F/hW9xP - NoQjW+6Rv7+jktA1eqpRbbW1BJzXcQldVWiJMxPNuEOg1iZ98SlvvTi1P3wnaWZc - eIapP7wRfs3QYaJuxCC/Pq2g0ieqALFazGAXkALOJtvujvw1Ea9XBlIjuzmyxEuh - Iwg+Gxx0g0f6yTquwax4YGECgYEAnpAK3qKFNO1ECzQDo8oNy0ep59MNDPtlDhQK - katJus5xdCD9oq7TQKrVOTTxZAvmzTQ1PqfuqueDVYOhD9Zg2n/P1cRlEGTek99Z - pfvppB/yak6+r3FA9yBKFS/r1zuMQg3nNweav62QV/tz5pT7AdeDMGFtaPlwtTYx - qyWY5aECgYBPySbPccNj+xxQzxcti2y/UXjC04RgOA/Hm1D0exa0vBqS9uxlOdG8 - F47rKenpBrslvdfTVsCDB1xyP2ebWVzp6EqMycw6OLPxgo3fBfZ4pi6P+rByh0Cc - Lhfh+ET0CPnKCxtop3lUrn4ZvqchS0j3J+M0pDuqoWF5hfKxFhkEIw== - -----END RSA PRIVATE KEY----- - providerPublicKey: | - -----BEGIN PUBLIC KEY----- - MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu+Kg/PHHU3atXrUbKnw0 - G06FliXcNt3lMwl2os5twEDcPPFw/feGiAKymxp+7JqZDrseS5D9THGrW+OQRIPH - WvUBdiLfGrZqJO223DB6D8K2Su/odmnjZJ2z23rhXoEArTplu+Dg9K+c2LVeXTKV - VPOaOzgtAB21XKRiQ4ermqgi3/njr03rXyq/qNkuNd6tNcg+HAfGxfGvvCSYBfiS - bUKr/BeArYRcjzr/h5m1In6fG/if9GEI6m8dxHT9JbY53wiksowy6ajCuqskIFg8 - 7X883H+LA/d6X5CTiPv1VMxXdBUiGPuC9IT/6CNQ1/LFt0P37ax58+LGYlaFo7la - nQIDAQAB - -----END PUBLIC KEY----- - providerPublicCert: | - -----BEGIN CERTIFICATE----- - MIIDdjCCAl4CCQCm0AiwERR/qjANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJE - RTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xGDAWBgNVBAoMD1dp - cmUgU3dpc3MgR21iSDERMA8GA1UEAwwId2lyZS5jb20xHzAdBgkqhkiG9w0BCQEW - EGJhY2tlbmRAd2lyZS5jb20wHhcNMTYwODA0MTMxNDQyWhcNMzYwNzMwMTMxNDQy - WjB9MQswCQYDVQQGEwJERTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJs - aW4xGDAWBgNVBAoMD1dpcmUgU3dpc3MgR21iSDERMA8GA1UEAwwId2lyZS5jb20x - HzAdBgkqhkiG9w0BCQEWEGJhY2tlbmRAd2lyZS5jb20wggEiMA0GCSqGSIb3DQEB - AQUAA4IBDwAwggEKAoIBAQC74qD88cdTdq1etRsqfDQbToWWJdw23eUzCXaizm3A - QNw88XD994aIArKbGn7smpkOux5LkP1Mcatb45BEg8da9QF2It8atmok7bbcMHoP - wrZK7+h2aeNknbPbeuFegQCtOmW74OD0r5zYtV5dMpVU85o7OC0AHbVcpGJDh6ua - qCLf+eOvTetfKr+o2S413q01yD4cB8bF8a+8JJgF+JJtQqv8F4CthFyPOv+HmbUi - fp8b+J/0YQjqbx3EdP0ltjnfCKSyjDLpqMK6qyQgWDztfzzcf4sD93pfkJOI+/VU - zFd0FSIY+4L0hP/oI1DX8sW3Q/ftrHnz4sZiVoWjuVqdAgMBAAEwDQYJKoZIhvcN - AQELBQADggEBAEuwlHElIGR56KVC1dJiw238mDGjMfQzSP76Wi4zWS6/zZwJUuog - BkC+vacfju8UAMvL+vdqkjOVUHor84/2wuq0qn91AjOITD7tRAZB+XLXxsikKv/v - OXE3A/lCiNi882NegPyXAfFPp/71CIiTQZps1eQkAvhD5t5WiFYPESxDlvEJrHFY - XP4+pp8fL8YPS7iZNIq+z+P8yVIw+B/Hs0ht7wFIYN0xACbU8m9+Rs08JMoT16c+ - hZMuK3BWD3fzkQVfW0yMwz6fWRXB483ZmekGkgndOTDoJQMdJXZxHpI3t2FcxQYj - T45GXxRd18neXtuYa/OoAw9UQFDN5XfXN0g= - -----END CERTIFICATE----- diff --git a/charts/gundeck/.helmignore b/charts/gundeck/.helmignore deleted file mode 100644 index f0c13194444..00000000000 --- a/charts/gundeck/.helmignore +++ /dev/null @@ -1,21 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*~ -# Various IDEs -.project -.idea/ -*.tmproj diff --git a/charts/gundeck/Chart.yaml b/charts/gundeck/Chart.yaml deleted file mode 100644 index 57548f45c6d..00000000000 --- a/charts/gundeck/Chart.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -description: Gundeck (part of Wire Server) - Push Notification Hub Service -name: gundeck -version: 0.0.42 diff --git a/charts/gundeck/README.md b/charts/gundeck/README.md deleted file mode 100644 index eafa3c5bcc2..00000000000 --- a/charts/gundeck/README.md +++ /dev/null @@ -1,6 +0,0 @@ -Note that gundeck depends on some provisioned storage, namely: - -- cassandra-all -- redis-gundeck - -These are dealt with independently from this chart. Ensure the `config.redis.host` and `config.cassandra.host` point to valid dns names. diff --git a/charts/gundeck/templates/_helpers.tpl b/charts/gundeck/templates/_helpers.tpl deleted file mode 100644 index e51069720fc..00000000000 --- a/charts/gundeck/templates/_helpers.tpl +++ /dev/null @@ -1,65 +0,0 @@ - -{{/* Allow KubeVersion to be overridden. */}} -{{- define "kubeVersion" -}} - {{- default .Capabilities.KubeVersion.Version .Values.kubeVersionOverride -}} -{{- end -}} - -{{- define "includeSecurityContext" -}} - {{- (semverCompare ">= 1.24-0" (include "kubeVersion" .)) -}} -{{- end -}} - -{{- define "useCassandraTLS" -}} -{{ or (hasKey .cassandra "tlsCa") (hasKey .cassandra "tlsCaSecretRef") }} -{{- end -}} - -{{/* Return a Dict of TLS CA secret name and key -This is used to switch between provided secret (e.g. by cert-manager) and -created one (in case the CA is provided as PEM string.) -*/}} -{{- define "tlsSecretRef" -}} -{{- if .cassandra.tlsCaSecretRef -}} -{{ .cassandra.tlsCaSecretRef | toYaml }} -{{- else }} -{{- dict "name" "gundeck-cassandra" "key" "ca.pem" | toYaml -}} -{{- end -}} -{{- end -}} - -{{- define "configureRedisCa" -}} -{{ or (hasKey .redis "tlsCa") (hasKey .redis "tlsCaSecretRef") }} -{{- end -}} - -{{- define "redisTlsSecretName" -}} -{{- if .redis.tlsCaSecretRef -}} -{{ .redis.tlsCaSecretRef.name }} -{{- else }} -{{- print "gundeck-redis-ca" -}} -{{- end -}} -{{- end -}} - -{{- define "redisTlsSecretKey" -}} -{{- if .redis.tlsCaSecretRef -}} -{{ .redis.tlsCaSecretRef.key }} -{{- else }} -{{- print "ca.pem" -}} -{{- end -}} -{{- end -}} - -{{- define "configureAdditionalRedisCa" -}} -{{ and (hasKey . "redisAdditionalWrite") (or (hasKey .redisAdditionalWrite "additionalTlsCa") (hasKey .redis "additionalTlsCaSecretRef")) }} -{{- end -}} - -{{- define "additionalRedisTlsSecretName" -}} -{{- if .redis.additionalTlsCaSecretRef -}} -{{ .redis.additionalTlsCaSecretRef.name }} -{{- else }} -{{- print "gundeck-additional-redis-ca" -}} -{{- end -}} -{{- end -}} - -{{- define "additionalRedisTlsSecretKey" -}} -{{- if .redis.additionalTlsCaSecretRef -}} -{{ .redis.additionalTlsCaSecretRef.key }} -{{- else }} -{{- print "ca.pem" -}} -{{- end -}} -{{- end -}} diff --git a/charts/gundeck/values.yaml b/charts/gundeck/values.yaml deleted file mode 100644 index 1a989c8510a..00000000000 --- a/charts/gundeck/values.yaml +++ /dev/null @@ -1,122 +0,0 @@ -replicaCount: 3 -image: - repository: quay.io/wire/gundeck - tag: do-not-use -service: - externalPort: 8080 - internalPort: 8080 -metrics: - serviceMonitor: - enabled: false -resources: - requests: - memory: "300Mi" - cpu: "100m" - limits: - memory: "1Gi" - -# Should be greater than Warp's graceful shutdown (default 30s). -terminationGracePeriodSeconds: 40 -config: - logLevel: Info - logFormat: StructuredJSON - logNetStrings: false - rabbitmq: - host: rabbitmq - port: 5672 - vHost: / - enableTls: false - insecureSkipVerifyTls: false - cassandra: - host: aws-cassandra - # To enable TLS provide a CA: - # tlsCa: - # - # Or refer to an existing secret (containing the CA): - # tlsCaSecretRef: - # name: - # key: - redis: - host: redis-ephemeral - port: 6379 - connectionMode: "master" # master | cluster - enableTls: false - insecureSkipVerifyTls: false - # To configure custom TLS CA, please provide one of these: - # tlsCa: - # - # Or refer to an existing secret (containing the CA): - # tlsCaSecretRef: - # name: - # key: - - # To enable additional writes during a migration: - # redisAdditionalWrite: - # host: redis-two - # port: 6379 - # connectionMode: master - # enableTls: false - # insecureSkipVerifyTls: false - # - # # To configure custom TLS CA, please provide one of these: - # # tlsCa: - # # - # # Or refer to an existing secret (containing the CA): - # # tlsCaSecretRef: - # # name: - # # key: - aws: - region: "eu-west-1" - proxy: {} - # perNativePushConcurrency: 32 - maxConcurrentNativePushes: - soft: 1000 - # Disable one ore more API versions. Please make sure the configuration value is the same in all these charts: - # brig, cannon, cargohold, galley, gundeck, proxy, spar. - disabledAPIVersions: [ development ] - - # Maximum number of bytes loaded into memory when fetching (referenced) payloads. - # Gundeck will return a truncated page if the whole page's payload sizes would exceed this limit in total. - # Inlined payloads can cause greater payload sizes to be loaded into memory regardless of this setting. - # Tune this to trade off memory usage VS number of requests for gundeck workers. - maxPayloadLoadSize: 5242880 - # Cassandra page size for fetching notifications. Does not directly effect the - # page size request in the client API. A lower number will reduce the amount - # by which setMaxPayloadLoadSize is exceeded when loading notifications from - # the database if notifications have inlined payloads. - internalPageSize: 100 - - # TTL of stored notifications in Seconds. After this period, notifications - # will be deleted and thus not delivered. - # The default is 28 days. - notificationTTL: 2419200 - - # To enable cells notifications - # cellsEventQueue: cells_events - -serviceAccount: - # When setting this to 'false', either make sure that a service account named - # 'gundeck' exists or change the 'name' field to 'default' - create: true - name: gundeck - annotations: {} - automountServiceAccountToken: true - -secrets: {} - -podSecurityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - runAsNonRoot: true - seccompProfile: - type: RuntimeDefault -tests: - config: {} -# config: -# uploadXml: -# baseUrl: s3://bucket/path/ -# secrets: -# uploadXmlAwsAccessKeyId: -# uploadXmlAwsSecretAccessKey: diff --git a/charts/metallb/.helmignore b/charts/metallb/.helmignore deleted file mode 100644 index f0c13194444..00000000000 --- a/charts/metallb/.helmignore +++ /dev/null @@ -1,21 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*~ -# Various IDEs -.project -.idea/ -*.tmproj diff --git a/charts/metallb/Chart.yaml b/charts/metallb/Chart.yaml deleted file mode 100644 index 551eb4096b3..00000000000 --- a/charts/metallb/Chart.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -description: A Helm chart for metallb on Kubernetes -name: metallb -version: 0.0.42 diff --git a/charts/metallb/README.md b/charts/metallb/README.md deleted file mode 100644 index cdc42403ca5..00000000000 --- a/charts/metallb/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# IMPORTANT -# -# Make sure you have a SINGLE metallb instance across the whole cluster, do not create multiple of these -# Have a read at the warning here: https://metallb.universe.tf/installation/ -# -# You only need to adjust values to the available IP address range and run -# -# helm upgrade --install --namespace metallb-system metallb charts/metallb \ -# -f values/metallb/values.yaml --wait --timeout 1800 -# diff --git a/charts/metallb/requirements.yaml b/charts/metallb/requirements.yaml deleted file mode 100644 index 4bff9ca9701..00000000000 --- a/charts/metallb/requirements.yaml +++ /dev/null @@ -1,4 +0,0 @@ -dependencies: -- name: metallb - version: 0.8.0 - repository: https://charts.helm.sh/stable diff --git a/charts/metallb/templates/configmap.yaml b/charts/metallb/templates/configmap.yaml deleted file mode 100644 index 60a306d83b9..00000000000 --- a/charts/metallb/templates/configmap.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - namespace: metallb-system - name: metallb-config -data: - config: | - address-pools: - - name: my-ip-space - protocol: layer2 - addresses: - {{- range .Values.cidrAddresses }} - - {{ . }} - {{- end }} diff --git a/charts/metallb/values.yaml b/charts/metallb/values.yaml deleted file mode 100644 index b130d6223be..00000000000 --- a/charts/metallb/values.yaml +++ /dev/null @@ -1,7 +0,0 @@ -# Adjust here the IP addresses that the LB can manage -# Note that there is no "validation" of these addresses -# so if you can bind to such addresses your services -# will be allocated bogus external IP addresses - -# cidrAddresses: -# - 10.0.0.0/32 diff --git a/charts/nginz/templates/conf/_nginx.conf.tpl b/charts/nginz/templates/conf/_nginx.conf.tpl index f6a2857ac37..671acd38869 100644 --- a/charts/nginz/templates/conf/_nginx.conf.tpl +++ b/charts/nginz/templates/conf/_nginx.conf.tpl @@ -64,7 +64,7 @@ http { # However we do not want to log access tokens. # - log_format custom_zeta '$remote_addr $remote_user "$time_local" "$sanitized_request" $status $body_bytes_sent "$http_referer" "$http_user_agent" $http_x_forwarded_for $connection $request_time $upstream_response_time $upstream_cache_status $zauth_user $zauth_connection $request_id $proxy_protocol_addr "$http_tracestate"'; + log_format custom_zeta '$remote_addr $remote_user "$time_local" "$sanitized_request" $status $body_bytes_sent "$http_referer" "$http_user_agent" $http_x_forwarded_for $connection $request_time $upstream_response_time $upstream_cache_status $zauth_user $zauth_connection $request_id $proxy_protocol_addr "$http_tracestate" "$http_wire_client" "$http_wire_client_version" "$http_wire_config_hash"'; access_log /dev/stdout custom_zeta; # @@ -176,10 +176,8 @@ http { limit_conn_zone $rate_limited_by_zuser zone=conns_per_user:10m; limit_conn_zone $rate_limited_by_addr zone=conns_per_addr:10m; - # Too Many Requests (420) is returned on throttling - # TODO: Change to 429 once all clients support this - limit_req_status 420; - limit_conn_status 420; + limit_req_status {{ .Values.nginx_conf.rate_limit_status }}; + limit_conn_status {{ .Values.nginx_conf.rate_limit_status }}; limit_req_log_level warn; limit_conn_log_level warn; diff --git a/charts/nginz/values.yaml b/charts/nginz/values.yaml index 4f01a47ad9a..20412031f42 100644 --- a/charts/nginz/values.yaml +++ b/charts/nginz/values.yaml @@ -89,6 +89,9 @@ nginx_conf: default_client_max_body_size: "512k" rate_limit_reqs_per_user: "30r/s" rate_limit_reqs_per_addr: "15r/m" + # The status code that is returned on throttling. + # 420 is the legacy status code and should be changed to 429 once all client support this + rate_limit_status: 420 # This value must be a list of strings. Each string is copied verbatim into # the nginx.conf after the default 'limit_req_zone' directives. This should be diff --git a/charts/proxy/.helmignore b/charts/proxy/.helmignore deleted file mode 100644 index f0c13194444..00000000000 --- a/charts/proxy/.helmignore +++ /dev/null @@ -1,21 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*~ -# Various IDEs -.project -.idea/ -*.tmproj diff --git a/charts/proxy/Chart.yaml b/charts/proxy/Chart.yaml deleted file mode 100644 index 04cb2a0b64c..00000000000 --- a/charts/proxy/Chart.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -description: Proxy (part of Wire Server) - 3rd party proxy service -name: proxy -version: 0.0.42 diff --git a/charts/proxy/templates/_helpers.tpl b/charts/proxy/templates/_helpers.tpl deleted file mode 100644 index af93ab9a720..00000000000 --- a/charts/proxy/templates/_helpers.tpl +++ /dev/null @@ -1,8 +0,0 @@ -{{/* Allow KubeVersion to be overridden. */}} -{{- define "kubeVersion" -}} - {{- default .Capabilities.KubeVersion.Version .Values.kubeVersionOverride -}} -{{- end -}} - -{{- define "includeSecurityContext" -}} - {{- (semverCompare ">= 1.24-0" (include "kubeVersion" .)) -}} -{{- end -}} diff --git a/charts/proxy/templates/configmap.yaml b/charts/proxy/templates/configmap.yaml deleted file mode 100644 index 1f07e080586..00000000000 --- a/charts/proxy/templates/configmap.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: "proxy" -data: - proxy.yaml: | - logFormat: {{ .Values.config.logFormat }} - logLevel: {{ .Values.config.logLevel }} - logNetStrings: {{ .Values.config.logNetStrings }} - disabledAPIVersions: {{ toJson .Values.config.disabledAPIVersions }} - proxy: - host: 0.0.0.0 - port: {{ .Values.service.internalPort }} - httpPoolSize: 1000 - maxConns: 5000 - secretsConfig: /etc/wire/proxy/secrets/proxy.config diff --git a/charts/proxy/values.yaml b/charts/proxy/values.yaml deleted file mode 100644 index 216851a1226..00000000000 --- a/charts/proxy/values.yaml +++ /dev/null @@ -1,33 +0,0 @@ -replicaCount: 3 -image: - repository: quay.io/wire/proxy - tag: do-not-use -service: - externalPort: 8080 - internalPort: 8080 -metrics: - serviceMonitor: - enabled: false -resources: - requests: - memory: "25Mi" - cpu: "50m" - limits: - memory: "50Mi" -config: - logLevel: Info - logFormat: StructuredJSON - logNetStrings: false - proxy: {} - # Disable one ore more API versions. Please make sure the configuration value is the same in all these charts: - # brig, cannon, cargohold, galley, gundeck, proxy, spar. - disabledAPIVersions: [ development ] - -podSecurityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - runAsNonRoot: true - seccompProfile: - type: RuntimeDefault diff --git a/charts/reaper/scripts/reaper.sh b/charts/reaper/scripts/reaper.sh index 517716902bb..d0f2679354e 100755 --- a/charts/reaper/scripts/reaper.sh +++ b/charts/reaper/scripts/reaper.sh @@ -21,7 +21,7 @@ kill_all_cannons() { } while IFS= read -r cannon; do - if [ -n "$cannon" ]; then + if [[ -n "$cannon" ]]; then echo "Deleting $cannon" # If a single delete fails, we skip it but keep going. kubectl -n "$NAMESPACE" delete pod "$cannon" || { @@ -59,7 +59,7 @@ while true; do # Check which is oldest FIRST_POD=$(echo "$ALL_PODS" | head -n 1 | awk '{ print $1 }') - if [ -z "$FIRST_POD" ]; then + if [[ -z "$FIRST_POD" ]]; then echo "Could not determine the oldest pod from the list. Doing nothing..." sleep 60 continue diff --git a/charts/spar/.helmignore b/charts/spar/.helmignore deleted file mode 100644 index f0c13194444..00000000000 --- a/charts/spar/.helmignore +++ /dev/null @@ -1,21 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*~ -# Various IDEs -.project -.idea/ -*.tmproj diff --git a/charts/spar/Chart.yaml b/charts/spar/Chart.yaml deleted file mode 100644 index 1bb0c413bea..00000000000 --- a/charts/spar/Chart.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -description: Spar (part of Wire Server) - SSO Service -name: spar -version: 0.0.42 diff --git a/charts/spar/templates/_helpers.tpl b/charts/spar/templates/_helpers.tpl deleted file mode 100644 index 8b28a7d9537..00000000000 --- a/charts/spar/templates/_helpers.tpl +++ /dev/null @@ -1,51 +0,0 @@ -{{/* Allow KubeVersion to be overridden. */}} -{{- define "kubeVersion" -}} - {{- default .Capabilities.KubeVersion.Version .Values.kubeVersionOverride -}} -{{- end -}} - -{{- define "includeSecurityContext" -}} - {{- (semverCompare ">= 1.24-0" (include "kubeVersion" .)) -}} -{{- end -}} - -{{- define "useCassandraTLS" -}} -{{ or (hasKey .cassandra "tlsCa") (hasKey .cassandra "tlsCaSecretRef") }} -{{- end -}} - -{{/* Return a Dict of TLS CA secret name and key -This is used to switch between provided secret (e.g. by cert-manager) and -created one (in case the CA is provided as PEM string.) -*/}} -{{- define "tlsSecretRef" -}} -{{- if .cassandra.tlsCaSecretRef -}} -{{ .cassandra.tlsCaSecretRef | toYaml }} -{{- else }} -{{- dict "name" "spar-cassandra" "key" "ca.pem" | toYaml -}} -{{- end -}} -{{- end -}} - -{{/* Compute the SCIM base URI - -The rules are: -- If `scimBaseUri` is defined, take that value -- Otherwise, if `ssoUri` is defined, take the value and drop a possible last - /sso path element -- Otherwise, fail - -In multi-ingress setups you have to configure the `scimBaseUri`, because it -cannot be decided which `ssoUri` to take from the map. -*/}} -{{- define "computeScimBaseUri" -}} -{{- if .scimBaseUri -}} - {{- .scimBaseUri -}} -{{- else if .ssoUri -}} - {{- $parts := splitList "/" .ssoUri -}} - {{- if eq (last $parts) "sso" -}} - {{- $baseUri := $parts | reverse | rest | reverse | join "/" -}} - {{- $baseUri -}}/scim/v2 - {{- else -}} - {{- .ssoUri -}}/scim/v2 - {{- end -}} -{{- else -}} - {{- fail "Either scimBaseUri or ssoUri must be defined" -}} -{{- end -}} -{{- end -}} diff --git a/charts/spar/values.yaml b/charts/spar/values.yaml deleted file mode 100644 index 1ff3160823a..00000000000 --- a/charts/spar/values.yaml +++ /dev/null @@ -1,96 +0,0 @@ -replicaCount: 3 -image: - repository: quay.io/wire/spar - tag: do-not-use -metrics: - serviceMonitor: - enabled: false -resources: - requests: - memory: "100Mi" - cpu: "50m" - limits: - memory: "200Mi" -service: - externalPort: 8080 - internalPort: 8080 -config: - cassandra: - host: aws-cassandra -# To enable TLS provide a CA: -# tlsCa: -# -# Or refer to an existing secret (containing the CA): -# tlsCaSecretRef: -# name: -# key: - richInfoLimit: 5000 - maxScimTokens: 0 - - # Enables the `/sso/get-by-email` endpoint which returns the matching IdP for - # an email address of a team member. - enableIdPByEmailDiscovery: false - - logLevel: Info - logFormat: StructuredJSON - logNetStrings: false - maxttlAuthreq: 7200 - maxttlAuthresp: 7200 - proxy: {} - - # Disable one ore more API versions. Please make sure the configuration value is the same in all these charts: - # brig, cannon, cargohold, galley, gundeck, proxy, spar. - disabledAPIVersions: [ development ] - - # SAML - ServiceProvider configuration - # Usually, one would configure one set of options for a single domain. For - # multi-ingress setups (one backend is available through multiple domains), - # configure a map with the nginz domain as key. - # - # Single domain configuration - # appUri: # E.g. https://webapp. - # ssoUri: # E.g. https://nginz-https./sso - # contacts: - # - type: # One of ContactTechnical, ContactSupport, ContactAdministrative, ContactBilling, ContactOther - # company: # Optional - # email: # Optional - # givenName: # Optional - # surname: # Optional - # phone: # Optional - # - # Multi-ingress configuration - # domainConfigs: - # : # The domain of the incoming nginz-https host. E.g. nginz-https. - # appUri: # E.g. https://webapp. - # ssoUri: # E.g. https://nginz-https./sso - # contacts: - # - type: # One of ContactTechnical, ContactSupport, ContactAdministrative, ContactBilling, ContactOther - # company: # Optional - # email: # Optional - # givenName: # Optional - # surname: # Optional - # phone: # Optional - # : - # ... - - # SCIM base URI (by default deduced from single-domain spSsoUri, must be set - # for multi-ingress) - # scimBaseUri: - -podSecurityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - runAsNonRoot: true - seccompProfile: - type: RuntimeDefault - -tests: - config: {} -# config: -# uploadXml: -# baseUrl: s3://bucket/path/ -# secrets: -# uploadXmlAwsAccessKeyId: -# uploadXmlAwsSecretAccessKey: diff --git a/charts/cannon/conf/static/zauth.acl b/charts/wire-server/conf/cannon-static/zauth.acl similarity index 100% rename from charts/cannon/conf/static/zauth.acl rename to charts/wire-server/conf/cannon-static/zauth.acl diff --git a/charts/wire-server/requirements.yaml b/charts/wire-server/requirements.yaml index 60ed93fcbe0..9b130743d70 100644 --- a/charts/wire-server/requirements.yaml +++ b/charts/wire-server/requirements.yaml @@ -22,55 +22,6 @@ dependencies: - backoffice - haskellServices - services -- name: cannon - version: "0.0.42" - repository: "file://../cannon" - tags: - - cannon - - haskellServices - - services -- name: proxy - version: "0.0.42" - repository: "file://../proxy" - tags: - - proxy - - haskellServices - - services -- name: cargohold - version: "0.0.42" - repository: "file://../cargohold" - tags: - - cargohold - - haskellServices - - services -- name: gundeck - version: "0.0.42" - repository: "file://../gundeck" - tags: - - gundeck - - haskellServices - - services -- name: spar - version: "0.0.42" - repository: "file://../spar" - tags: - - spar - - haskellServices - - services -- name: galley - version: "0.0.42" - repository: "file://../galley" - tags: - - galley - - haskellServices - - services -- name: brig - version: "0.0.42" - repository: "file://../brig" - tags: - - brig - - haskellServices - - services - name: nginz version: "0.0.42" repository: "file://../nginz" @@ -91,13 +42,6 @@ dependencies: - federation - haskellServices - services -- name: background-worker - version: "0.0.42" - repository: "file://../background-worker" - tags: - - background-worker - - haskellServices - - services - name: integration version: "0.0.42" repository: "file://../integration" diff --git a/charts/wire-server/templates/_helpers.tpl b/charts/wire-server/templates/_helpers.tpl new file mode 100644 index 00000000000..0cd6d5ede53 --- /dev/null +++ b/charts/wire-server/templates/_helpers.tpl @@ -0,0 +1,191 @@ + +{{/* SHARED HELPERS */}} +{{/* Allow KubeVersion to be overridden. */}} +{{- define "kubeVersion" -}} + {{- default .Capabilities.KubeVersion.Version .Values.kubeVersionOverride -}} +{{- end -}} +{{- define "includeSecurityContext" -}} + {{- (semverCompare ">= 1.24-0" (include "kubeVersion" .)) -}} +{{- end -}} + +{{- define "useCassandraTLS" -}} +{{ or (hasKey . "tlsCa") (hasKey . "tlsCaSecretRef") }} +{{- end -}} + +{{/* GALLEY */}} +{{- define "galley.tlsSecretRef" -}} +{{- if .cassandra.tlsCaSecretRef -}} +{{ .cassandra.tlsCaSecretRef | toYaml }} +{{- else }} +{{- dict "name" "galley-cassandra" "key" "ca.pem" | toYaml -}} +{{- end -}} +{{- end -}} + + +{{/* BACKGROUND-WORKER */}} +{{- define "gundeckTlsSecretRef" -}} +{{- if .cassandra.tlsCaSecretRef -}} +{{ .cassandra.tlsCaSecretRef | toYaml }} +{{- else }} +{{- dict "name" "background-worker-cassandra-gundeck" "key" "ca.pem" | toYaml -}} +{{- end -}} +{{- end -}} + +{{- define "brigTlsSecretRef" -}} +{{- if .cassandraBrig.tlsCaSecretRef -}} +{{ .cassandraBrig.tlsCaSecretRef | toYaml }} +{{- else }} +{{- dict "name" "background-worker-cassandra-brig" "key" "ca.pem" | toYaml -}} +{{- end -}} +{{- end -}} + +{{- define "galleyTlsSecretRef" -}} +{{- if and .cassandraGalley .cassandraGalley.tlsCaSecretRef -}} +{{ .cassandraGalley.tlsCaSecretRef | toYaml }} +{{- else }} +{{- dict "name" "background-worker-cassandra-galley" "key" "ca.pem" | toYaml -}} +{{- end -}} +{{- end -}} + +{{/* BRIG */}} +{{- define "brig.tlsSecretRef" -}} +{{- if .cassandra.tlsCaSecretRef -}} +{{ .cassandra.tlsCaSecretRef | toYaml }} +{{- else }} +{{- dict "name" "brig-cassandra" "key" "ca.pem" | toYaml -}} +{{- end -}} +{{- end -}} + +{{- define "brig.configureElasticSearchCa" -}} +{{ or (hasKey .elasticsearch "tlsCa") (hasKey .elasticsearch "tlsCaSecretRef") }} +{{- end -}} + +{{- define "brig.elasticsearchTlsSecretName" -}} +{{- if .elasticsearch.tlsCaSecretRef -}} +{{ .elasticsearch.tlsCaSecretRef.name }} +{{- else }} +{{- print "brig-elasticsearch-ca" -}} +{{- end -}} +{{- end -}} + +{{- define "brig.elasticsearchTlsSecretKey" -}} +{{- if .elasticsearch.tlsCaSecretRef -}} +{{ .elasticsearch.tlsCaSecretRef.key }} +{{- else }} +{{- print "ca.pem" -}} +{{- end -}} +{{- end -}} + +{{- define "brig.configureAdditionalElasticSearchCa" -}} +{{ or (hasKey .elasticsearch "additionalTlsCa") (hasKey .elasticsearch "additionalTlsCaSecretRef") }} +{{- end -}} + +{{- define "brig.additionalElasticsearchTlsSecretName" -}} +{{- if .elasticsearch.additionalTlsCaSecretRef -}} +{{ .elasticsearch.additionalTlsCaSecretRef.name }} +{{- else }} +{{- print "brig-additional-elasticsearch-ca" -}} +{{- end -}} +{{- end -}} + +{{- define "brig.additionalElasticsearchTlsSecretKey" -}} +{{- if .elasticsearch.additionalTlsCaSecretRef -}} +{{ .elasticsearch.additionalTlsCaSecretRef.key }} +{{- else }} +{{- print "ca.pem" -}} +{{- end -}} +{{- end -}} + +{{/* CANNON */}} +{{- define "cannon.tlsSecretRef" -}} +{{- if .cassandra.tlsCaSecretRef -}} +{{ .cassandra.tlsCaSecretRef | toYaml }} +{{- else }} +{{- dict "name" "cannon-cassandra" "key" "ca.pem" | toYaml -}} +{{- end -}} +{{- end -}} + +{{/* GUNDECK */}} +{{- define "gundeck.tlsSecretRef" -}} +{{- if .cassandra.tlsCaSecretRef -}} +{{ .cassandra.tlsCaSecretRef | toYaml }} +{{- else }} +{{- dict "name" "gundeck-cassandra" "key" "ca.pem" | toYaml -}} +{{- end -}} +{{- end -}} + +{{- define "gundeck.configureRedisCa" -}} +{{ or (hasKey .redis "tlsCa") (hasKey .redis "tlsCaSecretRef") }} +{{- end -}} + +{{- define "gundeck.redisTlsSecretName" -}} +{{- if .redis.tlsCaSecretRef -}} +{{ .redis.tlsCaSecretRef.name }} +{{- else }} +{{- print "gundeck-redis-ca" -}} +{{- end -}} +{{- end -}} + +{{- define "gundeck.redisTlsSecretKey" -}} +{{- if .redis.tlsCaSecretRef -}} +{{ .redis.tlsCaSecretRef.key }} +{{- else }} +{{- print "ca.pem" -}} +{{- end -}} +{{- end -}} + +{{- define "gundeck.configureAdditionalRedisCa" -}} +{{ and (hasKey . "redisAdditionalWrite") (or (hasKey .redis "additionalTlsCa") (hasKey .redis "additionalTlsCaSecretRef")) }} +{{- end -}} + +{{- define "gundeck.additionalRedisTlsSecretName" -}} +{{- if .redis.additionalTlsCaSecretRef -}} +{{ .redis.additionalTlsCaSecretRef.name }} +{{- else }} +{{- print "gundeck-additional-redis-ca" -}} +{{- end -}} +{{- end -}} + +{{- define "gundeck.additionalRedisTlsSecretKey" -}} +{{- if .redis.additionalTlsCaSecretRef -}} +{{ .redis.additionalTlsCaSecretRef.key }} +{{- else }} +{{- print "ca.pem" -}} +{{- end -}} +{{- end -}} + +{{/* SPAR */}} +{{- define "spar.tlsSecretRef" -}} +{{- if .cassandra.tlsCaSecretRef -}} +{{ .cassandra.tlsCaSecretRef | toYaml }} +{{- else }} +{{- dict "name" "spar-cassandra" "key" "ca.pem" | toYaml -}} +{{- end -}} +{{- end -}} + +{{/* Compute the SCIM base URI + +The rules are: +- If `scimBaseUri` is defined, take that value +- Otherwise, if `ssoUri` is defined, take the value and drop a possible last + /sso path element +- Otherwise, fail + +In multi-ingress setups you have to configure the `scimBaseUri`, because it +cannot be decided which `ssoUri` to take from the map. +*/}} +{{- define "spar.computeScimBaseUri" -}} +{{- if .scimBaseUri -}} + {{- .scimBaseUri -}} +{{- else if .ssoUri -}} + {{- $parts := splitList "/" .ssoUri -}} + {{- if eq (last $parts) "sso" -}} + {{- $baseUri := $parts | reverse | rest | reverse | join "/" -}} + {{- $baseUri -}}/scim/v2 + {{- else -}} + {{- .ssoUri -}}/scim/v2 + {{- end -}} +{{- else -}} + {{- fail "Either scimBaseUri or ssoUri must be defined" -}} +{{- end -}} +{{- end -}} diff --git a/charts/background-worker/templates/cassandra-secret.yaml b/charts/wire-server/templates/background-worker/cassandra-secret.yaml similarity index 61% rename from charts/background-worker/templates/cassandra-secret.yaml rename to charts/wire-server/templates/background-worker/cassandra-secret.yaml index 158d75ea548..8db998467ed 100644 --- a/charts/background-worker/templates/cassandra-secret.yaml +++ b/charts/wire-server/templates/background-worker/cassandra-secret.yaml @@ -1,5 +1,6 @@ {{/* Secrets for provided Cassandra TLS CAs */}} -{{- if not (empty .Values.config.cassandra.tlsCa) }} +{{- $backgroundWorker := index .Values "background-worker" }} +{{- if not (empty $backgroundWorker.config.cassandra.tlsCa) }} apiVersion: v1 kind: Secret metadata: @@ -11,9 +12,9 @@ metadata: heritage: "{{ .Release.Service }}" type: Opaque data: - ca.pem: {{ .Values.config.cassandra.tlsCa | b64enc | quote }} + ca.pem: {{ $backgroundWorker.config.cassandra.tlsCa | b64enc | quote }} {{- end }} -{{- if not (empty .Values.config.cassandraBrig.tlsCa) }} +{{- if and $backgroundWorker.config.cassandraBrig (not (empty $backgroundWorker.config.cassandraBrig.tlsCa)) }} --- apiVersion: v1 kind: Secret @@ -26,9 +27,9 @@ metadata: heritage: "{{ .Release.Service }}" type: Opaque data: - ca.pem: {{ .Values.config.cassandraBrig.tlsCa | b64enc | quote }} + ca.pem: {{ $backgroundWorker.config.cassandraBrig.tlsCa | b64enc | quote }} {{- end }} -{{- if and .Values.config.cassandraGalley (not (empty .Values.config.cassandraGalley.tlsCa)) }} +{{- if and $backgroundWorker.config.cassandraGalley (not (empty $backgroundWorker.config.cassandraGalley.tlsCa)) }} --- apiVersion: v1 kind: Secret @@ -41,5 +42,5 @@ metadata: heritage: "{{ .Release.Service }}" type: Opaque data: - ca.pem: {{ .Values.config.cassandraGalley.tlsCa | b64enc | quote }} + ca.pem: {{ $backgroundWorker.config.cassandraGalley.tlsCa | b64enc | quote }} {{- end }} diff --git a/charts/background-worker/templates/configmap.yaml b/charts/wire-server/templates/background-worker/configmap.yaml similarity index 83% rename from charts/background-worker/templates/configmap.yaml rename to charts/wire-server/templates/background-worker/configmap.yaml index 2ca739b4d61..49c0c3d38d7 100644 --- a/charts/background-worker/templates/configmap.yaml +++ b/charts/wire-server/templates/background-worker/configmap.yaml @@ -1,3 +1,4 @@ +{{- $backgroundWorker := index .Values "background-worker" }} apiVersion: v1 kind: ConfigMap metadata: @@ -8,14 +9,14 @@ metadata: release: {{ .Release.Name }} heritage: {{ .Release.Service }} data: - {{- with .Values.config }} + {{- with $backgroundWorker.config }} background-worker.yaml: | logFormat: {{ .logFormat }} logLevel: {{ .logLevel }} backgroundWorker: host: 0.0.0.0 - port: {{ $.Values.service.internalPort }} + port: {{ $backgroundWorker.service.internalPort }} federatorInternal: host: {{ .federatorInternal.host }} @@ -43,10 +44,11 @@ data: {{- if hasKey .cassandra "filterNodesByDatacentre" }} filterNodesByDatacentre: {{ .cassandra.filterNodesByDatacentre }} {{- end }} - {{- if eq (include "useGundeckCassandraTLS" .) "true" }} + {{- if eq (include "useCassandraTLS" .cassandra) "true" }} tlsCa: /etc/wire/background-worker/cassandra-gundeck/{{- (include "gundeckTlsSecretRef" . | fromYaml).key }} {{- end }} + {{- if .cassandraBrig }} cassandraBrig: endpoint: host: {{ .cassandraBrig.host }} @@ -55,10 +57,12 @@ data: {{- if hasKey .cassandraBrig "filterNodesByDatacentre" }} filterNodesByDatacentre: {{ .cassandraBrig.filterNodesByDatacentre }} {{- end }} - {{- if eq (include "useBrigCassandraTLS" .) "true" }} + {{- if eq (include "useCassandraTLS" .cassandraBrig) "true" }} tlsCa: /etc/wire/background-worker/cassandra-brig/{{- (include "brigTlsSecretRef" . | fromYaml).key }} {{- end }} + {{- end }} + {{- if .cassandraGalley }} cassandraGalley: endpoint: host: {{ .cassandraGalley.host }} @@ -67,24 +71,25 @@ data: {{- if hasKey .cassandraGalley "filterNodesByDatacentre" }} filterNodesByDatacentre: {{ .cassandraGalley.filterNodesByDatacentre }} {{- end }} - {{- if eq (include "useGalleyCassandraTLS" .) "true" }} + {{- if eq (include "useCassandraTLS" .cassandraGalley) "true" }} tlsCa: /etc/wire/background-worker/cassandra-galley/{{- (include "galleyTlsSecretRef" . | fromYaml).key }} {{- end }} + {{- end }} postgresql: {{ toYaml .postgresql | nindent 6 }} postgresqlPool: {{ toYaml .postgresqlPool | nindent 6 }} - {{- if hasKey $.Values.secrets "pgPassword" }} + {{- if hasKey $backgroundWorker.secrets "pgPassword" }} postgresqlPassword: /etc/wire/background-worker/secrets/pgPassword {{- end }} - federationDomain: {{ .federationDomain }} + federationDomain: {{ $backgroundWorker.config.federationDomain }} {{- with .rabbitmq }} rabbitmq: host: {{ .host }} port: {{ .port }} vHost: {{ .vHost }} - {{- if $.Values.config.enableFederation }} + {{- if $backgroundWorker.config.enableFederation }} adminHost: {{ default .host .adminHost }} adminPort: {{ .adminPort }} {{- end }} diff --git a/charts/background-worker/templates/deployment.yaml b/charts/wire-server/templates/background-worker/deployment.yaml similarity index 65% rename from charts/background-worker/templates/deployment.yaml rename to charts/wire-server/templates/background-worker/deployment.yaml index a90aba833a8..31ee47b1e55 100644 --- a/charts/background-worker/templates/deployment.yaml +++ b/charts/wire-server/templates/background-worker/deployment.yaml @@ -1,3 +1,4 @@ +{{- $backgroundWorker := index .Values "background-worker" }} apiVersion: apps/v1 kind: Deployment metadata: @@ -8,7 +9,7 @@ metadata: release: {{ .Release.Name }} heritage: {{ .Release.Service }} spec: - replicas: {{ .Values.replicaCount }} + replicas: {{ $backgroundWorker.replicaCount }} strategy: # Ensures only one version of the background worker is running at any given # moment. This means small downtime, but the background workers should be @@ -24,9 +25,9 @@ spec: release: {{ .Release.Name }} annotations: # An annotation of the configmap checksum ensures changes to the configmap cause a redeployment upon `helm upgrade` - checksum/configmap: {{ include (print .Template.BasePath "/configmap.yaml") . | sha256sum }} - checksum/secret: {{ include (print .Template.BasePath "/secret.yaml") . | sha256sum }} - checksum/cassandra-secret: {{ include (print .Template.BasePath "/cassandra-secret.yaml") . | sha256sum }} + checksum/configmap: {{ include (print .Template.BasePath "/background-worker/configmap.yaml") . | sha256sum }} + checksum/secret: {{ include (print .Template.BasePath "/background-worker/secret.yaml") . | sha256sum }} + checksum/cassandra-secret: {{ include (print .Template.BasePath "/background-worker/cassandra-secret.yaml") . | sha256sum }} fluentbit.io/parser: json spec: serviceAccount: null @@ -39,55 +40,55 @@ spec: - name: "background-worker-secrets" secret: secretName: "background-worker" - {{- if eq (include "useGundeckCassandraTLS" .Values.config) "true" }} + {{- if eq (include "useCassandraTLS" $backgroundWorker.config.cassandra) "true" }} - name: "background-worker-cassandra-gundeck" secret: - secretName: {{ (include "gundeckTlsSecretRef" .Values.config | fromYaml).name }} + secretName: {{ (include "gundeckTlsSecretRef" $backgroundWorker.config | fromYaml).name }} {{- end }} - {{- if eq (include "useBrigCassandraTLS" .Values.config) "true" }} + {{- if eq (include "useCassandraTLS" $backgroundWorker.config.cassandraBrig) "true" }} - name: "background-worker-cassandra-brig" secret: - secretName: {{ (include "brigTlsSecretRef" .Values.config | fromYaml).name }} + secretName: {{ (include "brigTlsSecretRef" $backgroundWorker.config | fromYaml).name }} {{- end }} - {{- if eq (include "useGalleyCassandraTLS" .Values.config) "true" }} + {{- if eq (include "useCassandraTLS" $backgroundWorker.config.cassandraGalley) "true" }} - name: "background-worker-cassandra-galley" secret: - secretName: {{ (include "galleyTlsSecretRef" .Values.config | fromYaml).name }} + secretName: {{ (include "galleyTlsSecretRef" $backgroundWorker.config | fromYaml).name }} {{- end }} - {{- if .Values.config.rabbitmq.tlsCaSecretRef }} + {{- if $backgroundWorker.config.rabbitmq.tlsCaSecretRef }} - name: "rabbitmq-ca" secret: - secretName: {{ .Values.config.rabbitmq.tlsCaSecretRef.name }} + secretName: {{ $backgroundWorker.config.rabbitmq.tlsCaSecretRef.name }} {{- end }} {{- if .Values.additionalVolumes }} {{ toYaml .Values.additionalVolumes | nindent 8 }} {{- end }} containers: - name: background-worker - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" - imagePullPolicy: {{ default "" .Values.imagePullPolicy | quote }} + image: "{{ $backgroundWorker.image.repository }}:{{ $backgroundWorker.image.tag }}" + imagePullPolicy: {{ default "" $backgroundWorker.imagePullPolicy | quote }} {{- if eq (include "includeSecurityContext" .) "true" }} securityContext: - {{- toYaml .Values.podSecurityContext | nindent 12 }} + {{- toYaml $backgroundWorker.podSecurityContext | nindent 12 }} {{- end }} volumeMounts: - name: "background-worker-secrets" mountPath: "/etc/wire/background-worker/secrets" - name: "background-worker-config" mountPath: "/etc/wire/background-worker/conf" - {{- if eq (include "useGundeckCassandraTLS" .Values.config) "true" }} + {{- if eq (include "useCassandraTLS" $backgroundWorker.config.cassandra) "true" }} - name: "background-worker-cassandra-gundeck" mountPath: "/etc/wire/background-worker/cassandra-gundeck" {{- end }} - {{- if eq (include "useBrigCassandraTLS" .Values.config) "true" }} + {{- if eq (include "useCassandraTLS" $backgroundWorker.config.cassandraBrig) "true" }} - name: "background-worker-cassandra-brig" mountPath: "/etc/wire/background-worker/cassandra-brig" {{- end }} - {{- if eq (include "useGalleyCassandraTLS" .Values.config) "true" }} + {{- if eq (include "useCassandraTLS" $backgroundWorker.config.cassandraGalley) "true" }} - name: "background-worker-cassandra-galley" mountPath: "/etc/wire/background-worker/cassandra-galley" {{- end }} - {{- if .Values.config.rabbitmq.tlsCaSecretRef }} + {{- if $backgroundWorker.config.rabbitmq.tlsCaSecretRef }} - name: "rabbitmq-ca" mountPath: "/etc/wire/background-worker/rabbitmq-ca/" {{- end }} @@ -109,18 +110,18 @@ spec: httpGet: scheme: HTTP path: /i/status - port: {{ .Values.service.internalPort }} + port: {{ $backgroundWorker.service.internalPort }} failureThreshold: 6 periodSeconds: 5 livenessProbe: httpGet: scheme: HTTP path: /i/status - port: {{ .Values.service.internalPort }} + port: {{ $backgroundWorker.service.internalPort }} readinessProbe: httpGet: scheme: HTTP path: /i/status - port: {{ .Values.service.internalPort }} + port: {{ $backgroundWorker.service.internalPort }} resources: -{{ toYaml .Values.resources | indent 12 }} +{{ toYaml $backgroundWorker.resources | indent 12 }} diff --git a/charts/background-worker/templates/secret.yaml b/charts/wire-server/templates/background-worker/secret.yaml similarity index 73% rename from charts/background-worker/templates/secret.yaml rename to charts/wire-server/templates/background-worker/secret.yaml index dfde355db9d..4fc5271160b 100644 --- a/charts/background-worker/templates/secret.yaml +++ b/charts/wire-server/templates/background-worker/secret.yaml @@ -1,3 +1,4 @@ +{{- $backgroundWorker := index .Values "background-worker" }} apiVersion: v1 kind: Secret metadata: @@ -9,10 +10,10 @@ metadata: heritage: "{{ .Release.Service }}" type: Opaque data: - {{/* for_helm_linting is necessary only since the 'with' block below does not throw an error upon an empty .Values.secrets */}} - for_helm_linting: {{ required "No .secrets found in configuration. Did you forget to helm -f path/to/secrets.yaml ?" .Values.secrets | quote | b64enc | quote }} + {{/* for_helm_linting is necessary only since the 'with' block below does not throw an error upon an empty .Values.background-worker.secrets.*/}} + for_helm_linting: {{ required "No .secrets found in configuration. Did you forget to helm -f path/to/secrets.yaml ?" $backgroundWorker.secrets | quote | b64enc | quote }} - {{- with .Values.secrets }} + {{- with $backgroundWorker.secrets }} rabbitmqUsername: {{ .rabbitmq.username | b64enc | quote }} rabbitmqPassword: {{ .rabbitmq.password | b64enc | quote }} {{- if .pgPassword }} diff --git a/charts/background-worker/templates/service.yaml b/charts/wire-server/templates/background-worker/service.yaml similarity index 75% rename from charts/background-worker/templates/service.yaml rename to charts/wire-server/templates/background-worker/service.yaml index 283fbce662d..05c6018dc42 100644 --- a/charts/background-worker/templates/service.yaml +++ b/charts/wire-server/templates/background-worker/service.yaml @@ -1,3 +1,4 @@ +{{- $backgroundWorker := index .Values "background-worker" }} apiVersion: v1 kind: Service metadata: @@ -17,8 +18,8 @@ spec: type: ClusterIP ports: - name: http - port: {{ .Values.service.externalPort }} - targetPort: {{ .Values.service.internalPort }} + port: {{ $backgroundWorker.service.externalPort }} + targetPort: {{ $backgroundWorker.service.internalPort }} selector: app: background-worker release: {{ .Release.Name }} diff --git a/charts/background-worker/templates/servicemonitor.yaml b/charts/wire-server/templates/background-worker/servicemonitor.yaml similarity index 78% rename from charts/background-worker/templates/servicemonitor.yaml rename to charts/wire-server/templates/background-worker/servicemonitor.yaml index 14dd65b488e..508fe5c392c 100644 --- a/charts/background-worker/templates/servicemonitor.yaml +++ b/charts/wire-server/templates/background-worker/servicemonitor.yaml @@ -1,4 +1,5 @@ -{{- if .Values.metrics.serviceMonitor.enabled }} +{{- $backgroundWorker := index .Values "background-worker" }} +{{- if $backgroundWorker.metrics.serviceMonitor.enabled }} apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: diff --git a/charts/brig/templates/cassandra-secret.yaml b/charts/wire-server/templates/brig/cassandra-secret.yaml similarity index 70% rename from charts/brig/templates/cassandra-secret.yaml rename to charts/wire-server/templates/brig/cassandra-secret.yaml index fa848001471..61b446edad8 100644 --- a/charts/brig/templates/cassandra-secret.yaml +++ b/charts/wire-server/templates/brig/cassandra-secret.yaml @@ -1,5 +1,5 @@ {{/* Secret for the provided Cassandra TLS CA. */}} -{{- if not (empty .Values.config.cassandra.tlsCa) }} +{{- if not (empty .Values.brig.config.cassandra.tlsCa) }} apiVersion: v1 kind: Secret metadata: @@ -11,5 +11,5 @@ metadata: heritage: "{{ .Release.Service }}" type: Opaque data: - ca.pem: {{ .Values.config.cassandra.tlsCa | b64enc | quote }} + ca.pem: {{ .Values.brig.config.cassandra.tlsCa | b64enc | quote }} {{- end }} diff --git a/charts/wire-server/templates/brig/conf/_turn-servers-v2.txt.tpl b/charts/wire-server/templates/brig/conf/_turn-servers-v2.txt.tpl new file mode 100644 index 00000000000..1c651e4c0c3 --- /dev/null +++ b/charts/wire-server/templates/brig/conf/_turn-servers-v2.txt.tpl @@ -0,0 +1,10 @@ +{{ define "brig.turn-servers-v2.txt" }} +{{- $turn := $.Values.brig.turn | default dict }} +{{- if eq ($turn.serversSource | default "files") "files" }} +{{- if not $.Values.brig.turnStatic }} +{{- fail "brig.turnStatic must be configured when turn.serversSource is 'files'" }} +{{- end }} +{{ range .Values.brig.turnStatic.v2 }}{{ . }} +{{ end -}} +{{- end }} +{{ end }} \ No newline at end of file diff --git a/charts/wire-server/templates/brig/conf/_turn-servers.txt.tpl b/charts/wire-server/templates/brig/conf/_turn-servers.txt.tpl new file mode 100644 index 00000000000..a7123580195 --- /dev/null +++ b/charts/wire-server/templates/brig/conf/_turn-servers.txt.tpl @@ -0,0 +1,10 @@ +{{ define "brig.turn-servers.txt" }} +{{- $turn := $.Values.brig.turn | default dict }} +{{- if eq ($turn.serversSource | default "files") "files" }} +{{- if not $.Values.brig.turnStatic }} +{{- fail "brig.turnStatic must be configured when turn.serversSource is 'files'" }} +{{- end }} +{{ range .Values.brig.turnStatic.v1 }}{{ . }} +{{ end -}} +{{- end }} +{{ end }} \ No newline at end of file diff --git a/charts/brig/templates/configmap.yaml b/charts/wire-server/templates/brig/configmap.yaml similarity index 91% rename from charts/brig/templates/configmap.yaml rename to charts/wire-server/templates/brig/configmap.yaml index 8b0bb33c1c6..66435c8d799 100644 --- a/charts/brig/templates/configmap.yaml +++ b/charts/wire-server/templates/brig/configmap.yaml @@ -8,7 +8,7 @@ metadata: release: {{ .Release.Name }} heritage: {{ .Release.Service }} data: - {{- with .Values.config }} + {{- with .Values.brig.config }} brig.yaml: | logNetStrings: {{ .logNetStrings }} logFormat: {{ .logFormat }} @@ -28,13 +28,13 @@ data: {{- if hasKey .cassandra "filterNodesByDatacentre" }} filterNodesByDatacentre: {{ .cassandra.filterNodesByDatacentre }} {{- end }} - {{- if eq (include "useCassandraTLS" .) "true" }} - tlsCa: /etc/wire/brig/cassandra/{{- (include "tlsSecretRef" . | fromYaml).key }} + {{- if eq (include "useCassandraTLS" .cassandra) "true" }} + tlsCa: /etc/wire/brig/cassandra/{{- (include "brig.tlsSecretRef" . | fromYaml).key }} {{- end }} postgresql: {{ toYaml .postgresql | nindent 6 }} postgresqlPool: {{ toYaml .postgresqlPool | nindent 6 }} - {{- if hasKey $.Values.secrets "pgPassword" }} + {{- if hasKey $.Values.brig.secrets "pgPassword" }} postgresqlPassword: /etc/wire/brig/secrets/pgPassword {{- end }} @@ -47,16 +47,16 @@ data: {{- if .elasticsearch.additionalWriteIndex }} additionalWriteIndex: {{ .elasticsearch.additionalWriteIndex }} {{- end }} - {{- if $.Values.secrets.elasticsearch }} + {{- if $.Values.brig.secrets.elasticsearch }} credentials: /etc/wire/brig/secrets/elasticsearch-credentials.yaml {{- end }} - {{- if eq (include "configureElasticSearchCa" .) "true" }} - caCert: /etc/wire/brig/elasticsearch-ca/{{ include "elasticsearchTlsSecretKey" .}} + {{- if eq (include "brig.configureElasticSearchCa" .) "true" }} + caCert: /etc/wire/brig/elasticsearch-ca/{{ include "brig.elasticsearchTlsSecretKey" .}} {{- end }} - {{- if eq (include "configureAdditionalElasticSearchCa" .) "true" }} - additionalCaCert: /etc/wire/brig/additional-elasticsearch-ca/{{ include "additionalElasticsearchTlsSecretKey" .}} + {{- if eq (include "brig.configureAdditionalElasticSearchCa" .) "true" }} + additionalCaCert: /etc/wire/brig/additional-elasticsearch-ca/{{ include "brig.additionalElasticsearchTlsSecretKey" .}} {{- end }} - {{- if $.Values.secrets.elasticsearchAdditional }} + {{- if $.Values.brig.secrets.elasticsearchAdditional }} additionalCredentials: /etc/wire/brig/secrets/elasticsearch-additional-credentials.yaml {{- end }} insecureSkipVerifyTls: {{ .elasticsearch.insecureSkipVerifyTls }} @@ -81,7 +81,7 @@ data: {{- if .multiSFT }} multiSFT: {{ .multiSFT.enabled }} {{- end }} - {{- if .enableFederation }} + {{- if $.Values.brig.config.enableFederation }} # TODO remove this federator: host: federator @@ -225,17 +225,18 @@ data: legalHoldAccessTokenTimeout: {{ .legalholdAccessTokenTimeout }} {{- end }} + {{- $turn := $.Values.brig.turn | default dict }} turn: - {{- if eq $.Values.turn.serversSource "dns" }} + {{- if eq ($turn.serversSource | default "files") "dns" }} serversSource: dns - baseDomain: {{ required ".turn.baseDomain must be configured if .turn.serversSource is set to dns" $.Values.turn.baseDomain }} - discoveryIntervalSeconds: {{ $.Values.turn.discoveryIntervalSeconds }} - {{- else if eq $.Values.turn.serversSource "files" }} + baseDomain: {{ required ".turn.baseDomain must be configured if .turn.serversSource is set to dns" $turn.baseDomain }} + discoveryIntervalSeconds: {{ $turn.discoveryIntervalSeconds }} + {{- else if eq ($turn.serversSource | default "files") "files" }} serversSource: files servers: /etc/wire/brig/turn/turn-servers.txt serversV2: /etc/wire/brig/turn/turn-servers-v2.txt {{- else }} - {{- fail (cat "Invalid value for .turn.serversSource, expected dns or files, got: " $.Values.turn.serversSource) }} + {{- fail (cat "Invalid value for .turn.serversSource, expected dns or files, got: " $turn.serversSource) }} {{- end }} secret: /etc/wire/brig/secrets/turn-secret.txt configTTL: 3600 # 1 hour @@ -251,7 +252,7 @@ data: {{- if .sftDiscoveryIntervalSeconds }} sftDiscoveryIntervalSeconds: {{ .sftDiscoveryIntervalSeconds }} {{- end }} - {{- if $.Values.secrets.sftTokenSecret }} + {{- if $.Values.brig.secrets.sftTokenSecret }} sftToken: {{- with .sftToken }} ttl: {{ .ttl }} @@ -297,7 +298,7 @@ data: setPropertyMaxKeyLen: {{ .setPropertyMaxKeyLen }} setPropertyMaxValueLen: {{ .setPropertyMaxValueLen }} setDeleteThrottleMillis: {{ .setDeleteThrottleMillis }} - setFederationDomain: {{ .setFederationDomain }} + setFederationDomain: {{ $.Values.brig.config.optSettings.setFederationDomain }} {{- if .setFederationStrategy }} setFederationStrategy: {{ .setFederationStrategy }} {{- end }} @@ -358,13 +359,13 @@ data: {{- if .setDpopTokenExpirationTimeSecs }} setDpopTokenExpirationTimeSecs: {{ .setDpopTokenExpirationTimeSecs }} {{- end }} - {{- if $.Values.secrets.dpopSigKeyBundle }} + {{- if $.Values.brig.secrets.dpopSigKeyBundle }} setPublicKeyBundle: /etc/wire/brig/secrets/dpop_sig_key_bundle.pem {{- end }} {{- if .setEnableMLS }} setEnableMLS: {{ .setEnableMLS }} {{- end }} - {{- if $.Values.secrets.oauthJwkKeyPair }} + {{- if $.Values.brig.secrets.oauthJwkKeyPair }} setOAuthJwkKeyPair: /etc/wire/brig/secrets/oauth_ed25519.jwk {{- end }} {{- if .setOAuthAuthCodeExpirationTimeSecs }} @@ -393,5 +394,6 @@ data: {{- if hasKey . "setNomadProfiles" }} setNomadProfiles: {{ index . "setNomadProfiles" }} {{- end }} + setConsumableNotifications: false {{- end }} {{- end }} diff --git a/charts/brig/templates/deployment.yaml b/charts/wire-server/templates/brig/deployment.yaml similarity index 61% rename from charts/brig/templates/deployment.yaml rename to charts/wire-server/templates/brig/deployment.yaml index 32b2ad473e8..cbf6b56ff75 100644 --- a/charts/brig/templates/deployment.yaml +++ b/charts/wire-server/templates/brig/deployment.yaml @@ -8,12 +8,12 @@ metadata: release: {{ .Release.Name }} heritage: {{ .Release.Service }} spec: - replicas: {{ .Values.replicaCount }} + replicas: {{ .Values.brig.replicaCount }} strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 0 - maxSurge: {{ .Values.replicaCount }} + maxSurge: {{ .Values.brig.replicaCount }} selector: matchLabels: app: brig @@ -24,12 +24,12 @@ spec: release: {{ .Release.Name }} annotations: # An annotation of the configmap checksum ensures changes to the configmap cause a redeployment upon `helm upgrade` - checksum/configmap: {{ include (print .Template.BasePath "/configmap.yaml") . | sha256sum }} - checksum/turnconfigmap: {{ include (print .Template.BasePath "/turnconfigmap.yaml") . | sha256sum }} - checksum/secret: {{ include (print .Template.BasePath "/secret.yaml") . | sha256sum }} + checksum/configmap: {{ include (print .Template.BasePath "/brig/configmap.yaml") . | sha256sum }} + checksum/turnconfigmap: {{ include (print .Template.BasePath "/brig/turnconfigmap.yaml") . | sha256sum }} + checksum/secret: {{ include (print .Template.BasePath "/brig/secret.yaml") . | sha256sum }} fluentbit.io/parser: json spec: - serviceAccountName: {{ .Values.serviceAccount.name }} + serviceAccountName: {{ .Values.brig.serviceAccount.name }} topologySpreadConstraints: - maxSkew: 1 topologyKey: "kubernetes.io/hostname" @@ -44,74 +44,75 @@ spec: - name: "brig-secrets" secret: secretName: "brig" - {{- if eq $.Values.turn.serversSource "files" }} + {{- $turn := $.Values.brig.turn | default dict }} + {{- if eq ($turn.serversSource | default "files") "files" }} - name: "turn-servers" configMap: name: "turn" {{- end }} - {{- if eq (include "useCassandraTLS" .Values.config) "true" }} + {{- if eq (include "useCassandraTLS" .Values.brig.config.cassandra) "true" }} - name: "brig-cassandra" secret: - secretName: {{ (include "tlsSecretRef" .Values.config | fromYaml).name }} + secretName: {{ (include "brig.tlsSecretRef" .Values.brig.config | fromYaml).name }} {{- end}} - {{- if eq (include "configureElasticSearchCa" .Values.config) "true" }} + {{- if eq (include "brig.configureElasticSearchCa" .Values.brig.config) "true" }} - name: "elasticsearch-ca" secret: - secretName: {{ include "elasticsearchTlsSecretName" .Values.config }} + secretName: {{ include "brig.elasticsearchTlsSecretName" .Values.brig.config }} {{- end }} - {{- if eq (include "configureAdditionalElasticSearchCa" .Values.config) "true" }} + {{- if eq (include "brig.configureAdditionalElasticSearchCa" .Values.brig.config) "true" }} - name: "additional-elasticsearch-ca" secret: - secretName: {{ include "additionalElasticsearchTlsSecretName" .Values.config }} + secretName: {{ include "brig.additionalElasticsearchTlsSecretName" .Values.brig.config }} {{- end }} - {{- if .Values.config.rabbitmq.tlsCaSecretRef }} + {{- if and .Values.brig.config.rabbitmq .Values.brig.config.rabbitmq.tlsCaSecretRef }} - name: "rabbitmq-ca" secret: - secretName: {{ .Values.config.rabbitmq.tlsCaSecretRef.name }} + secretName: {{ .Values.brig.config.rabbitmq.tlsCaSecretRef.name }} {{- end }} - {{- if .Values.additionalVolumes }} - {{ toYaml .Values.additionalVolumes | nindent 8 }} + {{- if .Values.brig.additionalVolumes }} + {{ toYaml .Values.brig.additionalVolumes | nindent 8 }} {{- end }} containers: - name: brig - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" - imagePullPolicy: {{ default "" .Values.imagePullPolicy | quote }} + image: "{{ .Values.brig.image.repository }}:{{ .Values.brig.image.tag }}" + imagePullPolicy: {{ default "" .Values.brig.imagePullPolicy | quote }} {{- if eq (include "includeSecurityContext" .) "true" }} securityContext: - {{- toYaml .Values.podSecurityContext | nindent 12 }} + {{- toYaml .Values.brig.podSecurityContext | nindent 12 }} {{- end }} volumeMounts: - name: "brig-secrets" mountPath: "/etc/wire/brig/secrets" - name: "brig-config" mountPath: "/etc/wire/brig/conf" - {{- if eq $.Values.turn.serversSource "files" }} + {{- if eq ($turn.serversSource | default "files") "files" }} - name: "turn-servers" mountPath: "/etc/wire/brig/turn" {{- end }} - {{- if eq (include "useCassandraTLS" .Values.config) "true" }} + {{- if eq (include "useCassandraTLS" .Values.brig.config.cassandra) "true" }} - name: "brig-cassandra" mountPath: "/etc/wire/brig/cassandra" {{- end }} - {{- if eq (include "configureElasticSearchCa" .Values.config) "true" }} + {{- if eq (include "brig.configureElasticSearchCa" .Values.brig.config) "true" }} - name: "elasticsearch-ca" mountPath: "/etc/wire/brig/elasticsearch-ca/" {{- end }} - {{- if eq (include "configureAdditionalElasticSearchCa" .Values.config) "true" }} + {{- if eq (include "brig.configureAdditionalElasticSearchCa" .Values.brig.config) "true" }} - name: "additional-elasticsearch-ca" mountPath: "/etc/wire/brig/additional-elasticsearch-ca/" {{- end }} - {{- if .Values.config.rabbitmq.tlsCaSecretRef }} + {{- if and .Values.brig.config.rabbitmq .Values.brig.config.rabbitmq.tlsCaSecretRef }} - name: "rabbitmq-ca" mountPath: "/etc/wire/brig/rabbitmq-ca/" {{- end }} - {{- if .Values.additionalVolumeMounts }} - {{ toYaml .Values.additionalVolumeMounts | nindent 10 }} + {{- if .Values.brig.additionalVolumeMounts }} + {{ toYaml .Values.brig.additionalVolumeMounts | nindent 10 }} {{- end }} env: - name: LOG_LEVEL - value: {{ .Values.config.logLevel }} - {{- if hasKey .Values.secrets "awsKeyId" }} + value: {{ .Values.brig.config.logLevel }} + {{- if hasKey .Values.brig.secrets "awsKeyId" }} - name: AWS_ACCESS_KEY_ID valueFrom: secretKeyRef: @@ -125,8 +126,8 @@ spec: {{- end }} # TODO: Is this the best way to do this? - name: AWS_REGION - value: "{{ .Values.config.aws.region }}" - {{- with .Values.config.proxy }} + value: "{{ .Values.brig.config.aws.region }}" + {{- with .Values.brig.config.proxy }} {{- if .httpProxy }} - name: http_proxy value: {{ .httpProxy | quote }} @@ -157,12 +158,12 @@ spec: name: brig key: rabbitmqPassword ports: - - containerPort: {{ .Values.service.internalPort }} + - containerPort: {{ .Values.brig.service.internalPort }} startupProbe: httpGet: scheme: HTTP path: /i/status - port: {{ .Values.service.internalPort }} + port: {{ .Values.brig.service.internalPort }} failureThreshold: 6 periodSeconds: 5 livenessProbe: @@ -172,13 +173,13 @@ spec: - -c - | set -e - curl -f http://localhost:{{ .Values.service.internalPort }}/i/status 2>/dev/null + curl -f http://localhost:{{ .Values.brig.service.internalPort }}/i/status 2>/dev/null - metrics=$(curl -f http://localhost:{{ .Values.service.internalPort }}/i/metrics 2>/dev/null) + metrics=$(curl -f http://localhost:{{ .Values.brig.service.internalPort }}/i/metrics 2>/dev/null) net_connections=$(echo "$metrics" | grep -o "net_connections [0-9]*" | grep -o "[0-9]*") - if [ "$net_connections" -gt "{{ .Values.livenessProbe.maxConnections }}" ]; then - echo "ERROR: net_connections ($net_connections) exceeds MAX_CONNECTIONS ({{ .Values.livenessProbe.maxConnections }})" + if [ "$net_connections" -gt "{{ .Values.brig.livenessProbe.maxConnections }}" ]; then + echo "ERROR: net_connections ($net_connections) exceeds MAX_CONNECTIONS ({{ .Values.brig.livenessProbe.maxConnections }})" exit 1 fi failureThreshold: 3 @@ -188,11 +189,11 @@ spec: httpGet: scheme: HTTP path: /i/status - port: {{ .Values.service.internalPort }} - {{- if .Values.preStop }} + port: {{ .Values.brig.service.internalPort }} + {{- if .Values.brig.preStop }} lifecycle: preStop: -{{ toYaml .Values.preStop | indent 14 }} +{{ toYaml .Values.brig.preStop | indent 14 }} {{- end }} resources: -{{ toYaml .Values.resources | indent 12 }} +{{ toYaml .Values.brig.resources | indent 12 }} diff --git a/charts/brig/templates/elasticsearch-ca-secret.yaml b/charts/wire-server/templates/brig/elasticsearch-ca-secret.yaml similarity index 65% rename from charts/brig/templates/elasticsearch-ca-secret.yaml rename to charts/wire-server/templates/brig/elasticsearch-ca-secret.yaml index 3c64b0e92d8..cc080f7c7c9 100644 --- a/charts/brig/templates/elasticsearch-ca-secret.yaml +++ b/charts/wire-server/templates/brig/elasticsearch-ca-secret.yaml @@ -1,5 +1,5 @@ --- -{{- if not (empty .Values.config.elasticsearch.tlsCa) }} +{{- if not (empty .Values.brig.config.elasticsearch.tlsCa) }} apiVersion: v1 kind: Secret metadata: @@ -11,10 +11,10 @@ metadata: heritage: "{{ .Release.Service }}" type: Opaque data: - ca.pem: {{ .Values.elasticsearch.tlsCa | b64enc | quote }} + ca.pem: {{ .Values.brig.config.elasticsearch.tlsCa | b64enc | quote }} {{- end }} --- -{{- if not (empty .Values.config.elasticsearch.additionalTlsCa) }} +{{- if not (empty .Values.brig.config.elasticsearch.additionalTlsCa) }} apiVersion: v1 kind: Secret metadata: @@ -26,5 +26,5 @@ metadata: heritage: "{{ .Release.Service }}" type: Opaque data: - ca.pem: {{ .Values.elasticsearch.additionalTlsCa | b64enc | quote }} + ca.pem: {{ .Values.brig.config.elasticsearch.additionalTlsCa | b64enc | quote }} {{- end }} diff --git a/charts/brig/templates/poddisruptionbudget.yaml b/charts/wire-server/templates/brig/poddisruptionbudget.yaml similarity index 73% rename from charts/brig/templates/poddisruptionbudget.yaml rename to charts/wire-server/templates/brig/poddisruptionbudget.yaml index 1230da256f1..091b3cf2f58 100644 --- a/charts/brig/templates/poddisruptionbudget.yaml +++ b/charts/wire-server/templates/brig/poddisruptionbudget.yaml @@ -1,4 +1,4 @@ -{{- if gt (int .Values.replicaCount) 1 }} +{{- if gt (int .Values.brig.replicaCount) 1 }} apiVersion: policy/v1 kind: PodDisruptionBudget metadata: @@ -9,7 +9,7 @@ metadata: release: {{ .Release.Name }} heritage: {{ .Release.Service }} spec: - maxUnavailable: {{ sub (int .Values.replicaCount) 1 }} + maxUnavailable: {{ sub (int .Values.brig.replicaCount) 1 }} selector: matchLabels: app: brig diff --git a/charts/brig/templates/secret.yaml b/charts/wire-server/templates/brig/secret.yaml similarity index 88% rename from charts/brig/templates/secret.yaml rename to charts/wire-server/templates/brig/secret.yaml index 57543a97ab8..5175276fda1 100644 --- a/charts/brig/templates/secret.yaml +++ b/charts/wire-server/templates/brig/secret.yaml @@ -9,10 +9,10 @@ metadata: heritage: "{{ .Release.Service }}" type: Opaque data: - {{/* for_helm_linting is necessary only since the 'with' block below does not throw an error upon an empty .Values.secrets */}} - for_helm_linting: {{ required "No .secrets found in configuration. Did you forget to helm -f path/to/secrets.yaml ?" .Values.secrets | quote | b64enc | quote }} + {{/* for_helm_linting is necessary only since the 'with' block below does not throw an error upon an empty .Values.brig.secrets */}} + for_helm_linting: {{ required "No .secrets found in configuration. Did you forget to helm -f path/to/secrets.yaml ?" .Values.brig.secrets | quote | b64enc | quote }} - {{- with .Values.secrets }} + {{- with .Values.brig.secrets }} secretkey.txt: {{ .zAuth.privateKeys | b64enc | quote }} publickey.txt: {{ .zAuth.publicKeys | b64enc | quote }} turn-secret.txt: {{ .turn.secret | b64enc | quote }} @@ -23,7 +23,7 @@ data: {{- if .sftTokenSecret }} sftTokenSecret: {{ .sftTokenSecret | b64enc | quote }} {{- end }} - {{- if (not $.Values.config.useSES) }} + {{- if (not $.Values.brig.config.useSES) }} smtp-password.txt: {{ .smtpPassword | b64enc | quote }} {{- end }} {{- if .dpopSigKeyBundle }} diff --git a/charts/brig/templates/service.yaml b/charts/wire-server/templates/brig/service.yaml similarity index 82% rename from charts/brig/templates/service.yaml rename to charts/wire-server/templates/brig/service.yaml index 58811c48195..780f84ec681 100644 --- a/charts/brig/templates/service.yaml +++ b/charts/wire-server/templates/brig/service.yaml @@ -17,8 +17,8 @@ spec: type: ClusterIP ports: - name: http - port: {{ .Values.service.externalPort }} - targetPort: {{ .Values.service.internalPort }} + port: {{ .Values.brig.service.externalPort }} + targetPort: {{ .Values.brig.service.internalPort }} selector: app: brig release: {{ .Release.Name }} diff --git a/charts/brig/templates/serviceaccount.yaml b/charts/wire-server/templates/brig/serviceaccount.yaml similarity index 53% rename from charts/brig/templates/serviceaccount.yaml rename to charts/wire-server/templates/brig/serviceaccount.yaml index bc120b624d8..d06774442b2 100644 --- a/charts/brig/templates/serviceaccount.yaml +++ b/charts/wire-server/templates/brig/serviceaccount.yaml @@ -1,16 +1,16 @@ -{{- if .Values.serviceAccount.create -}} +{{- if .Values.brig.serviceAccount.create -}} apiVersion: v1 kind: ServiceAccount metadata: - name: {{ .Values.serviceAccount.name }} + name: {{ .Values.brig.serviceAccount.name }} labels: app: brig chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} - {{- with .Values.serviceAccount.annotations }} + {{- with .Values.brig.serviceAccount.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} -automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} +automountServiceAccountToken: {{ .Values.brig.serviceAccount.automountServiceAccountToken }} {{- end }} diff --git a/charts/brig/templates/servicemonitor.yaml b/charts/wire-server/templates/brig/servicemonitor.yaml similarity index 87% rename from charts/brig/templates/servicemonitor.yaml rename to charts/wire-server/templates/brig/servicemonitor.yaml index 03c0b872442..5754eb5f4c7 100644 --- a/charts/brig/templates/servicemonitor.yaml +++ b/charts/wire-server/templates/brig/servicemonitor.yaml @@ -1,4 +1,4 @@ -{{- if .Values.metrics.serviceMonitor.enabled }} +{{- if .Values.brig.metrics.serviceMonitor.enabled }} apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: diff --git a/charts/brig/templates/tests/brig-integration.yaml b/charts/wire-server/templates/brig/tests/brig-integration.yaml similarity index 80% rename from charts/brig/templates/tests/brig-integration.yaml rename to charts/wire-server/templates/brig/tests/brig-integration.yaml index 433ded2177a..c2c9372217b 100644 --- a/charts/brig/templates/tests/brig-integration.yaml +++ b/charts/wire-server/templates/brig/tests/brig-integration.yaml @@ -44,27 +44,27 @@ spec: - name: "brig-integration-secrets" secret: secretName: "brig-integration" - {{- if eq (include "configureElasticSearchCa" .Values.config) "true" }} + {{- if eq (include "brig.configureElasticSearchCa" .Values.brig.config) "true" }} - name: elasticsearch-ca secret: - secretName: {{ include "elasticsearchTlsSecretName" .Values.config }} + secretName: {{ include "brig.elasticsearchTlsSecretName" .Values.brig.config }} {{- end}} - {{- if eq (include "useCassandraTLS" .Values.config) "true" }} + {{- if eq (include "useCassandraTLS" .Values.brig.config.cassandra) "true" }} - name: "brig-cassandra" secret: - secretName: {{ (include "tlsSecretRef" .Values.config | fromYaml).name }} + secretName: {{ (include "brig.tlsSecretRef" .Values.brig.config | fromYaml).name }} {{- end}} - {{- if .Values.config.rabbitmq.tlsCaSecretRef }} + {{- if .Values.brig.config.rabbitmq.tlsCaSecretRef }} - name: "rabbitmq-ca" secret: - secretName: {{ .Values.config.rabbitmq.tlsCaSecretRef.name }} + secretName: {{ .Values.brig.config.rabbitmq.tlsCaSecretRef.name }} {{- end }} containers: - name: integration - image: "{{ .Values.image.repository }}-integration:{{ .Values.image.tag }}" + image: "{{ .Values.brig.image.repository }}-integration:{{ .Values.brig.image.tag }}" {{- if eq (include "includeSecurityContext" .) "true" }} securityContext: - {{- toYaml .Values.podSecurityContext | nindent 6 }} + {{- toYaml .Values.brig.podSecurityContext | nindent 6 }} {{- end }} # TODO: Add TURN tests once we have an actual way to test it # The brig-integration tests mutate the turn settings files before tests @@ -85,7 +85,7 @@ spec: exit_code=$? fi - {{- if .Values.tests.config.uploadXml }} + {{- if .Values.brig.tests.config.uploadXml }} # In case a different S3 compliant storage is used to upload test result. if ! [[ -z "${UPLOAD_XML_AWS_ACCESS_KEY_ID+x}" ]]; then export AWS_ACCESS_KEY_ID="$UPLOAD_XML_AWS_ACCESS_KEY_ID" @@ -116,15 +116,15 @@ spec: # non-default locations # (see corresp. TODO in galley.) mountPath: "/etc/wire/integration-secrets" - {{- if eq (include "configureElasticSearchCa" .Values.config) "true" }} + {{- if eq (include "brig.configureElasticSearchCa" .Values.brig.config) "true" }} - name: elasticsearch-ca mountPath: "/etc/wire/brig/elasticsearch-ca" {{- end}} - {{- if eq (include "useCassandraTLS" .Values.config) "true" }} + {{- if eq (include "useCassandraTLS" .Values.brig.config.cassandra) "true" }} - name: "brig-cassandra" mountPath: "/etc/wire/brig/cassandra" {{- end }} - {{- if .Values.config.rabbitmq.tlsCaSecretRef }} + {{- if .Values.brig.config.rabbitmq.tlsCaSecretRef }} - name: "rabbitmq-ca" mountPath: "/etc/wire/brig/rabbitmq-ca/" {{- end }} @@ -143,10 +143,10 @@ spec: value: "guest" - name: TEST_XML value: /tmp/result.xml - {{- if .Values.tests.config.uploadXml }} + {{- if .Values.brig.tests.config.uploadXml }} - name: UPLOAD_XML_S3_BASE_URL - value: {{ .Values.tests.config.uploadXml.baseUrl }} - {{- if .Values.tests.secrets.uploadXmlAwsAccessKeyId }} + value: {{ .Values.brig.tests.config.uploadXml.baseUrl }} + {{- if .Values.brig.tests.secrets.uploadXmlAwsAccessKeyId }} - name: UPLOAD_XML_AWS_ACCESS_KEY_ID valueFrom: secretKeyRef: diff --git a/charts/brig/templates/tests/configmap.yaml b/charts/wire-server/templates/brig/tests/configmap.yaml similarity index 93% rename from charts/brig/templates/tests/configmap.yaml rename to charts/wire-server/templates/brig/tests/configmap.yaml index a1ad007f8a8..e7540dc7c59 100644 --- a/charts/brig/templates/tests/configmap.yaml +++ b/charts/wire-server/templates/brig/tests/configmap.yaml @@ -11,7 +11,7 @@ data: # Full URL is set so that there can be a common cookiedomain between nginz and brig # needed by some integration tests host: brig.{{ .Release.Namespace }}.svc.cluster.local - port: {{ .Values.service.internalPort }} + port: {{ .Values.brig.service.internalPort }} cannon: host: cannon @@ -90,4 +90,4 @@ data: host: federator.{{ .Release.Namespace }}-fed2.svc.cluster.local port: 8081 - additionalElasticSearch: https://{{ .Values.test.elasticsearch.additionalHost }}:9200 + additionalElasticSearch: https://{{ .Values.brig.test.elasticsearch.additionalHost }}:9200 diff --git a/charts/brig/templates/tests/nginz-service.yaml b/charts/wire-server/templates/brig/tests/nginz-service.yaml similarity index 100% rename from charts/brig/templates/tests/nginz-service.yaml rename to charts/wire-server/templates/brig/tests/nginz-service.yaml diff --git a/charts/brig/templates/tests/secret.yaml b/charts/wire-server/templates/brig/tests/secret.yaml similarity index 95% rename from charts/brig/templates/tests/secret.yaml rename to charts/wire-server/templates/brig/tests/secret.yaml index 5c5459e609a..86177dd7d3e 100644 --- a/charts/brig/templates/tests/secret.yaml +++ b/charts/wire-server/templates/brig/tests/secret.yaml @@ -12,7 +12,7 @@ metadata: "helm.sh/hook-delete-policy": before-hook-creation type: Opaque data: - {{- with .Values.tests.secrets }} + {{- with .Values.brig.tests.secrets }} provider-privatekey.pem: {{ .providerPrivateKey | b64enc | quote }} provider-publickey.pem: {{ .providerPublicKey | b64enc | quote }} provider-publiccert.pem: {{ .providerPublicCert | b64enc | quote }} diff --git a/charts/brig/templates/turnconfigmap.yaml b/charts/wire-server/templates/brig/turnconfigmap.yaml similarity index 56% rename from charts/brig/templates/turnconfigmap.yaml rename to charts/wire-server/templates/brig/turnconfigmap.yaml index 7a62071b578..3f0dd6b7a7d 100644 --- a/charts/brig/templates/turnconfigmap.yaml +++ b/charts/wire-server/templates/brig/turnconfigmap.yaml @@ -1,4 +1,5 @@ -{{- if eq $.Values.turn.serversSource "files" }} +{{- $turn := $.Values.brig.turn | default dict }} +{{- if eq ($turn.serversSource | default "files") "files" }} apiVersion: v1 kind: ConfigMap metadata: @@ -10,7 +11,7 @@ metadata: heritage: {{ .Release.Service }} data: turn-servers.txt: |2 -{{- include "turn-servers.txt" . | indent 4 }} +{{- include "brig.turn-servers.txt" . | indent 4 }} turn-servers-v2.txt: |2 -{{- include "turn-servers-v2.txt" . | indent 4 }} +{{- include "brig.turn-servers-v2.txt" . | indent 4 }} {{- end }} diff --git a/charts/cannon/templates/cassandra-secret.yaml b/charts/wire-server/templates/cannon/cassandra-secret.yaml similarity index 70% rename from charts/cannon/templates/cassandra-secret.yaml rename to charts/wire-server/templates/cannon/cassandra-secret.yaml index f5dcbf1a7c8..76c84f763e5 100644 --- a/charts/cannon/templates/cassandra-secret.yaml +++ b/charts/wire-server/templates/cannon/cassandra-secret.yaml @@ -1,5 +1,5 @@ {{/* Secret for the provided Cassandra TLS CA. */}} -{{- if not (empty .Values.config.cassandra.tlsCa) }} +{{- if not (empty .Values.cannon.config.cassandra.tlsCa) }} apiVersion: v1 kind: Secret metadata: @@ -11,5 +11,5 @@ metadata: heritage: "{{ .Release.Service }}" type: Opaque data: - ca.pem: {{ .Values.config.cassandra.tlsCa | b64enc | quote }} + ca.pem: {{ .Values.cannon.config.cassandra.tlsCa | b64enc | quote }} {{- end }} diff --git a/charts/cannon/templates/conf/_nginx.conf.tpl b/charts/wire-server/templates/cannon/conf/_nginx.conf.tpl similarity index 79% rename from charts/cannon/templates/conf/_nginx.conf.tpl rename to charts/wire-server/templates/cannon/conf/_nginx.conf.tpl index d359b082c66..0fe060f4c74 100644 --- a/charts/cannon/templates/conf/_nginx.conf.tpl +++ b/charts/wire-server/templates/cannon/conf/_nginx.conf.tpl @@ -1,6 +1,6 @@ {{- define "cannon_nginz_nginx.conf" }} -worker_processes {{ .Values.nginx_conf.worker_processes }}; -worker_rlimit_nofile {{ .Values.nginx_conf.worker_rlimit_nofile | default 1024 }}; +worker_processes {{ .Values.cannon.nginx_conf.worker_processes }}; +worker_rlimit_nofile {{ .Values.cannon.nginx_conf.worker_rlimit_nofile | default 1024 }}; pid /var/run/nginz.pid; # nb. start up errors (eg. misconfiguration) may still end up in @@ -8,7 +8,7 @@ pid /var/run/nginz.pid; error_log stderr warn; events { - worker_connections {{ .Values.nginx_conf.worker_connections | default 1024 }}; + worker_connections {{ .Values.cannon.nginx_conf.worker_connections | default 1024 }}; multi_accept off; use epoll; } @@ -95,15 +95,15 @@ http { geo $rate_limit { default 1; - # IPs to exempt can be added in the .Values.nginx_conf.rate_limit and .Values.nginx_conf.simulators helm values - {{ if (hasKey .Values.nginx_conf "rate_limit_exemptions") }} - {{ range $ip := .Values.nginx_conf.rate_limit_exemptions }} + # IPs to exempt can be added in the .Values.cannon.nginx_conf.rate_limit and .Values.cannon.nginx_conf.simulators helm values + {{ if (hasKey .Values.cannon.nginx_conf "rate_limit_exemptions") }} + {{ range $ip := .Values.cannon.nginx_conf.rate_limit_exemptions }} {{ $ip }} 0; {{ end }} {{ end }} - {{ if (hasKey .Values.nginx_conf "simulators") }} - {{ range $ip := .Values.nginx_conf.simulators }} + {{ if (hasKey .Values.cannon.nginx_conf "simulators") }} + {{ range $ip := .Values.cannon.nginx_conf.simulators }} {{ $ip }} 0; {{ end }} {{ end }} @@ -125,10 +125,10 @@ http { map $http_origin $cors_header { default ""; - {{ range $origin := .Values.nginx_conf.allowlisted_origins }} + {{ range $origin := .Values.cannon.nginx_conf.allowlisted_origins }} {{- range $domain := (prepend - $.Values.nginx_conf.additional_external_env_domains - $.Values.nginx_conf.external_env_domain) + $.Values.cannon.nginx_conf.additional_external_env_domains + $.Values.cannon.nginx_conf.external_env_domain) -}} "https://{{ $origin }}.{{ $domain }}" "$http_origin"; {{ end }} @@ -136,7 +136,7 @@ http { # Allow additional origins at random ports. This is useful for testing with an HTTP proxy. # It should not be used in production. - {{ range $origin := .Values.nginx_conf.randomport_allowlisted_origins }} + {{ range $origin := .Values.cannon.nginx_conf.randomport_allowlisted_origins }} "~^https?://{{ $origin }}(:[0-9]{2,5})?$" "$http_origin"; {{ end }} } @@ -146,20 +146,18 @@ http { # Rate Limiting # - limit_req_zone $rate_limited_by_zuser zone=reqs_per_user:12m rate={{ .Values.nginx_conf.rate_limit_reqs_per_user }}; - limit_req_zone $rate_limited_by_addr zone=reqs_per_addr:12m rate={{ .Values.nginx_conf.rate_limit_reqs_per_addr }}; + limit_req_zone $rate_limited_by_zuser zone=reqs_per_user:12m rate={{ .Values.cannon.nginx_conf.rate_limit_reqs_per_user }}; + limit_req_zone $rate_limited_by_addr zone=reqs_per_addr:12m rate={{ .Values.cannon.nginx_conf.rate_limit_reqs_per_addr }}; -{{- range $limit := .Values.nginx_conf.user_rate_limit_request_zones }} +{{- range $limit := .Values.cannon.nginx_conf.user_rate_limit_request_zones }} {{ $limit }} {{- end }} limit_conn_zone $rate_limited_by_zuser zone=conns_per_user:10m; limit_conn_zone $rate_limited_by_addr zone=conns_per_addr:10m; - # Too Many Requests (420) is returned on throttling - # TODO: Change to 429 once all clients support this - limit_req_status 420; - limit_conn_status 420; + limit_req_status {{ .Values.cannon.nginx_conf.rate_limit_status }}; + limit_conn_status {{ .Values.cannon.nginx_conf.rate_limit_status }}; limit_req_log_level warn; limit_conn_log_level warn; @@ -175,7 +173,7 @@ http { upstream cannon { least_conn; keepalive 32; - server localhost:{{ .Values.service.internalPort }}; + server localhost:{{ .Values.cannon.service.internalPort }}; } # @@ -194,22 +192,22 @@ http { # server { - listen {{ .Values.service.nginz.internalPort }} ssl; + listen {{ .Values.cannon.service.nginz.internalPort }} ssl; ssl_certificate /etc/wire/nginz/tls/tls.crt; ssl_certificate_key /etc/wire/nginz/tls/tls.key; - ssl_protocols {{ .Values.nginx_conf.tls.protocols }}; - ssl_ciphers {{ .Values.nginx_conf.tls.ciphers_tls12 }}; # this only sets TLS 1.2 ciphers (and has no effect if TLS 1.2 is not enabled) - ssl_conf_command Ciphersuites {{ .Values.nginx_conf.tls.ciphers_tls13 }}; # needed to override TLS 1.3 ciphers. + ssl_protocols {{ .Values.cannon.nginx_conf.tls.protocols }}; + ssl_ciphers {{ .Values.cannon.nginx_conf.tls.ciphers_tls12 }}; # this only sets TLS 1.2 ciphers (and has no effect if TLS 1.2 is not enabled) + ssl_conf_command Ciphersuites {{ .Values.cannon.nginx_conf.tls.ciphers_tls13 }}; # needed to override TLS 1.3 ciphers. # Disable session resumption. See comments in SQPIT-226 for more context and # discussion. ssl_session_tickets off; ssl_session_cache off; - zauth_keystore {{ .Values.nginx_conf.zauth_keystore }}; - zauth_acl {{ .Values.nginx_conf.zauth_acl }}; + zauth_keystore {{ .Values.cannon.nginx_conf.zauth_keystore }}; + zauth_acl {{ .Values.cannon.nginx_conf.zauth_acl }}; add_header Strict-Transport-Security 'max-age=31536000; includeSubdomains; preload' always; @@ -248,7 +246,7 @@ http { return 403; } - {{ range $path := .Values.nginx_conf.disabled_paths }} + {{ range $path := .Values.cannon.nginx_conf.disabled_paths }} location ~* ^(/v[0-9]+)?{{ $path }} { return 404; @@ -259,11 +257,11 @@ http { # Service Routing # - {{ range $name, $locations := .Values.nginx_conf.upstreams -}} + {{ range $name, $locations := .Values.cannon.nginx_conf.upstreams -}} {{- range $location := $locations -}} {{- if hasKey $location "envs" -}} {{- range $env := $location.envs -}} - {{- if or (eq $env $.Values.nginx_conf.env) (eq $env "all") -}} + {{- if or (eq $env $.Values.cannon.nginx_conf.env) (eq $env "all") -}} {{- if $location.strip_version }} @@ -306,7 +304,7 @@ http { return 204; } - proxy_pass http://{{ $name }}{{ if hasKey $.Values.nginx_conf.upstream_namespace $name }}.{{ get $.Values.nginx_conf.upstream_namespace $name }}{{end}}; + proxy_pass http://{{ $name }}{{ if hasKey $.Values.cannon.nginx_conf.upstream_namespace $name }}.{{ get $.Values.cannon.nginx_conf.upstream_namespace $name }}{{end}}; proxy_http_version 1.1; {{- if ($location.disable_request_buffering) }} diff --git a/charts/cannon/templates/configmap.yaml b/charts/wire-server/templates/cannon/configmap.yaml similarity index 86% rename from charts/cannon/templates/configmap.yaml rename to charts/wire-server/templates/cannon/configmap.yaml index e7fb94bfba5..bc69b36859c 100644 --- a/charts/cannon/templates/configmap.yaml +++ b/charts/wire-server/templates/cannon/configmap.yaml @@ -1,6 +1,6 @@ apiVersion: v1 data: - {{- with .Values }} + {{- with .Values.cannon }} cannon.yaml: | logFormat: {{ .config.logFormat }} logLevel: {{ .config.logLevel }} @@ -20,8 +20,8 @@ data: host: {{ .config.cassandra.host }} port: 9042 keyspace: gundeck - {{- if eq (include "useCassandraTLS" .config) "true" }} - tlsCa: /etc/wire/cannon/cassandra/{{- (include "tlsSecretRef" .config | fromYaml).key }} + {{- if eq (include "useCassandraTLS" .config.cassandra) "true" }} + tlsCa: /etc/wire/cannon/cassandra/{{- (include "cannon.tlsSecretRef" .config | fromYaml).key }} {{- end }} {{- with .config.rabbitmq }} diff --git a/charts/cannon/templates/headless-service.yaml b/charts/wire-server/templates/cannon/headless-service.yaml similarity index 86% rename from charts/cannon/templates/headless-service.yaml rename to charts/wire-server/templates/cannon/headless-service.yaml index e753a88c674..9897e7ba817 100644 --- a/charts/cannon/templates/headless-service.yaml +++ b/charts/wire-server/templates/cannon/headless-service.yaml @@ -7,7 +7,7 @@ apiVersion: v1 kind: Service metadata: - name: {{ .Values.service.name }} + name: {{ .Values.cannon.service.name }} labels: app: cannon chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} @@ -25,8 +25,8 @@ spec: clusterIP: None ports: - name: http - port: {{ .Values.service.externalPort }} - targetPort: {{ .Values.service.internalPort }} + port: {{ .Values.cannon.service.externalPort }} + targetPort: {{ .Values.cannon.service.internalPort }} protocol: TCP selector: app: cannon diff --git a/charts/wire-server/templates/cannon/nginz-certificate-secret.yaml b/charts/wire-server/templates/cannon/nginz-certificate-secret.yaml new file mode 100644 index 00000000000..0708449cdbc --- /dev/null +++ b/charts/wire-server/templates/cannon/nginz-certificate-secret.yaml @@ -0,0 +1,15 @@ +{{- if and .Values.cannon.service.nginz.enabled (not .Values.cannon.service.nginz.certManager.enabled ) }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Values.cannon.service.nginz.tls.secretName }} + labels: + app: cannon-nginz + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +type: kubernetes.io/tls +data: + tls.crt: {{ .Values.cannon.secrets.nginz.tls.crt | b64enc | quote }} + tls.key: {{ .Values.cannon.secrets.nginz.tls.key | b64enc | quote }} +{{- end }} diff --git a/charts/cannon/templates/nginz-certificate.yaml b/charts/wire-server/templates/cannon/nginz-certificate.yaml similarity index 62% rename from charts/cannon/templates/nginz-certificate.yaml rename to charts/wire-server/templates/cannon/nginz-certificate.yaml index 4245befdfbf..cd90e5ce6bc 100644 --- a/charts/cannon/templates/nginz-certificate.yaml +++ b/charts/wire-server/templates/cannon/nginz-certificate.yaml @@ -1,8 +1,8 @@ -{{- if and .Values.service.nginz.enabled .Values.service.nginz.certManager.enabled -}} +{{- if and .Values.cannon.service.nginz.enabled .Values.cannon.service.nginz.certManager.enabled -}} apiVersion: cert-manager.io/v1 kind: Certificate metadata: - name: {{ .Values.service.nginz.certManager.certificate.name }} + name: {{ .Values.cannon.service.nginz.certManager.certificate.name }} namespace: {{ .Release.Namespace }} labels: chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" @@ -10,14 +10,14 @@ metadata: heritage: "{{ .Release.Service }}" spec: issuerRef: - name: {{ .Values.service.nginz.certManager.issuer.name }} - kind: {{ .Values.service.nginz.certManager.issuer.kind }} + name: {{ .Values.cannon.service.nginz.certManager.issuer.name }} + kind: {{ .Values.cannon.service.nginz.certManager.issuer.kind }} usages: - server auth duration: 2160h # 90d, Letsencrypt default; NOTE: changes are ignored by Letsencrypt renewBefore: 360h # 15d isCA: false - secretName: {{ .Values.service.nginz.tls.secretName }} + secretName: {{ .Values.cannon.service.nginz.tls.secretName }} privateKey: algorithm: ECDSA @@ -26,5 +26,5 @@ spec: rotationPolicy: Always dnsNames: - - {{ required "Please provide .service.nginz.hostname when .service.nginz.enabled and .service.nginz.certManager.enabled are True" .Values.service.nginz.hostname | quote }} + - {{ required "Please provide .service.nginz.hostname when .service.nginz.enabled and .service.nginz.certManager.enabled are True" .Values.cannon.service.nginz.hostname | quote }} {{- end -}} diff --git a/charts/cannon/templates/nginz-configmap.yaml b/charts/wire-server/templates/cannon/nginz-configmap.yaml similarity index 57% rename from charts/cannon/templates/nginz-configmap.yaml rename to charts/wire-server/templates/cannon/nginz-configmap.yaml index 9c946455c95..480c0f4fd6c 100644 --- a/charts/cannon/templates/nginz-configmap.yaml +++ b/charts/wire-server/templates/cannon/nginz-configmap.yaml @@ -1,4 +1,4 @@ -{{- if .Values.service.nginz.enabled }} +{{- if .Values.cannon.service.nginz.enabled }} apiVersion: v1 kind: ConfigMap metadata: @@ -6,5 +6,5 @@ metadata: data: nginx.conf: |2 {{- include "cannon_nginz_nginx.conf" . | indent 4 }} -{{ (.Files.Glob "conf/static/*").AsConfig | indent 2 }} +{{ (.Files.Glob "conf/cannon-static/*").AsConfig | indent 2 }} {{- end }} diff --git a/charts/cannon/templates/nginz-secret.yaml b/charts/wire-server/templates/cannon/nginz-secret.yaml similarity index 70% rename from charts/cannon/templates/nginz-secret.yaml rename to charts/wire-server/templates/cannon/nginz-secret.yaml index 0670f7fe272..43d7fa5c81c 100644 --- a/charts/cannon/templates/nginz-secret.yaml +++ b/charts/wire-server/templates/cannon/nginz-secret.yaml @@ -1,4 +1,4 @@ -{{- if .Values.service.nginz.enabled }} +{{- if .Values.cannon.service.nginz.enabled }} apiVersion: v1 kind: Secret metadata: @@ -10,10 +10,10 @@ metadata: heritage: "{{ .Release.Service }}" type: Opaque data: - {{/* for_helm_linting is necessary only since the 'with' block below does not throw an error upon an empty .Values.secrets */}} - for_helm_linting: {{ required "No .secrets found in configuration. Did you forget to helm -f path/to/secrets.yaml ?" .Values.secrets | quote | b64enc | quote }} + {{/* for_helm_linting is necessary only since the 'with' block below does not throw an error upon an empty .Values.cannon.secrets */}} + for_helm_linting: {{ required "No .secrets found in configuration. Did you forget to helm -f path/to/secrets.yaml ?" .Values.cannon.secrets | quote | b64enc | quote }} - {{- with .Values.secrets.nginz }} + {{- with .Values.cannon.secrets.nginz }} zauth.conf: {{ .zAuth.publicKeys | b64enc | quote }} {{- end }} {{- end }} diff --git a/charts/cannon/templates/nginz-service.yaml b/charts/wire-server/templates/cannon/nginz-service.yaml similarity index 73% rename from charts/cannon/templates/nginz-service.yaml rename to charts/wire-server/templates/cannon/nginz-service.yaml index ea0ba2cfedb..bf919177bf3 100644 --- a/charts/cannon/templates/nginz-service.yaml +++ b/charts/wire-server/templates/cannon/nginz-service.yaml @@ -1,4 +1,4 @@ -{{- if .Values.service.nginz.enabled }} +{{- if .Values.cannon.service.nginz.enabled }} # This service has to be exposed using type `LoadBalancer` to ensure that there # is no other pod between the load balancer and this service. This ensures that # only thing which disrupts the websocket connection is when a cannon pod gets @@ -11,7 +11,7 @@ apiVersion: v1 kind: Service metadata: - name: {{ .Values.service.nginz.name }} + name: {{ .Values.cannon.service.nginz.name }} labels: app: cannon chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} @@ -23,11 +23,11 @@ metadata: {{- else }} service.kubernetes.io/topology-aware-hints: auto {{- end }} - {{- if .Values.service.nginz.externalDNS.enabled }} - external-dns.alpha.kubernetes.io/ttl: {{ .Values.service.nginz.externalDNS.ttl | quote }} - external-dns.alpha.kubernetes.io/hostname: {{ required "Please provide .service.nginz.hostname when .service.nginz.enabled and .service.nginz.externalDNS.enabled are True" .Values.service.nginz.hostname | quote }} + {{- if .Values.cannon.service.nginz.externalDNS.enabled }} + external-dns.alpha.kubernetes.io/ttl: {{ .Values.cannon.service.nginz.externalDNS.ttl | quote }} + external-dns.alpha.kubernetes.io/hostname: {{ required "Please provide .service.nginz.hostname when .service.nginz.enabled and .service.nginz.externalDNS.enabled are True" .Values.cannon.service.nginz.hostname | quote }} {{- end }} -{{ toYaml .Values.service.nginz.annotations | indent 4 }} +{{ toYaml .Values.cannon.service.nginz.annotations | indent 4 }} spec: type: LoadBalancer # This ensures websocket traffic does not go from one kubernetes node to @@ -36,8 +36,8 @@ spec: externalTrafficPolicy: "Local" ports: - name: http - port: {{ .Values.service.nginz.externalPort }} - targetPort: {{ .Values.service.nginz.internalPort }} + port: {{ .Values.cannon.service.nginz.externalPort }} + targetPort: {{ .Values.cannon.service.nginz.internalPort }} protocol: TCP selector: app: cannon diff --git a/charts/cannon/templates/poddisruptionbudget.yaml b/charts/wire-server/templates/cannon/poddisruptionbudget.yaml similarity index 72% rename from charts/cannon/templates/poddisruptionbudget.yaml rename to charts/wire-server/templates/cannon/poddisruptionbudget.yaml index def4bca8373..c741992e87f 100644 --- a/charts/cannon/templates/poddisruptionbudget.yaml +++ b/charts/wire-server/templates/cannon/poddisruptionbudget.yaml @@ -1,4 +1,4 @@ -{{- if gt (int .Values.replicaCount) 1 }} +{{- if gt (int .Values.cannon.replicaCount) 1 }} apiVersion: policy/v1 kind: PodDisruptionBudget metadata: @@ -9,7 +9,7 @@ metadata: release: {{ .Release.Name }} heritage: {{ .Release.Service }} spec: - maxUnavailable: {{ sub (int .Values.replicaCount) 1 }} + maxUnavailable: {{ sub (int .Values.cannon.replicaCount) 1 }} selector: matchLabels: app: cannon diff --git a/charts/cannon/templates/secret.yaml b/charts/wire-server/templates/cannon/secret.yaml similarity index 57% rename from charts/cannon/templates/secret.yaml rename to charts/wire-server/templates/cannon/secret.yaml index 1b6f9ebd94e..82435284b0b 100644 --- a/charts/cannon/templates/secret.yaml +++ b/charts/wire-server/templates/cannon/secret.yaml @@ -9,6 +9,6 @@ metadata: heritage: "{{ .Release.Service }}" type: Opaque data: - rabbitmqUsername: {{ .Values.secrets.rabbitmq.username | b64enc | quote }} - rabbitmqPassword: {{ .Values.secrets.rabbitmq.password | b64enc | quote }} + rabbitmqUsername: {{ .Values.cannon.secrets.rabbitmq.username | b64enc | quote }} + rabbitmqPassword: {{ .Values.cannon.secrets.rabbitmq.password | b64enc | quote }} diff --git a/charts/cannon/templates/servicemonitor.yaml b/charts/wire-server/templates/cannon/servicemonitor.yaml similarity index 87% rename from charts/cannon/templates/servicemonitor.yaml rename to charts/wire-server/templates/cannon/servicemonitor.yaml index df91d18654a..d068ef5db26 100644 --- a/charts/cannon/templates/servicemonitor.yaml +++ b/charts/wire-server/templates/cannon/servicemonitor.yaml @@ -1,4 +1,4 @@ -{{- if .Values.metrics.serviceMonitor.enabled }} +{{- if .Values.cannon.metrics.serviceMonitor.enabled }} apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: diff --git a/charts/cannon/templates/statefulset.yaml b/charts/wire-server/templates/cannon/statefulset.yaml similarity index 67% rename from charts/cannon/templates/statefulset.yaml rename to charts/wire-server/templates/cannon/statefulset.yaml index 5036eb4c159..ecc842b511a 100644 --- a/charts/cannon/templates/statefulset.yaml +++ b/charts/wire-server/templates/cannon/statefulset.yaml @@ -14,11 +14,11 @@ metadata: release: {{ .Release.Name }} heritage: {{ .Release.Service }} spec: - serviceName: {{ .Values.service.name }} + serviceName: {{ .Values.cannon.service.name }} selector: matchLabels: app: cannon - replicas: {{ .Values.replicaCount }} + replicas: {{ .Values.cannon.replicaCount }} updateStrategy: type: RollingUpdate podManagementPolicy: Parallel @@ -28,13 +28,13 @@ spec: app: cannon release: {{ .Release.Name }} annotations: - checksum/configmap: {{ include (print .Template.BasePath "/configmap.yaml") . | sha256sum }} - {{- if .Values.service.nginz.enabled }} - checksum/nginz-configmap: {{ include (print .Template.BasePath "/nginz-configmap.yaml") . | sha256sum }} + checksum/configmap: {{ include (print .Template.BasePath "/cannon/configmap.yaml") . | sha256sum }} + {{- if .Values.cannon.service.nginz.enabled }} + checksum/nginz-configmap: {{ include (print .Template.BasePath "/cannon/nginz-configmap.yaml") . | sha256sum }} {{- end }} - checksum/cassandra-secret: {{ include (print .Template.BasePath "/cassandra-secret.yaml") . | sha256sum }} + checksum/cassandra-secret: {{ include (print .Template.BasePath "/cannon/cassandra-secret.yaml") . | sha256sum }} spec: - terminationGracePeriodSeconds: {{ add .Values.config.drainOpts.gracePeriodSeconds 5 }} + terminationGracePeriodSeconds: {{ add .Values.cannon.config.drainOpts.gracePeriodSeconds 5 }} topologySpreadConstraints: - maxSkew: 1 topologyKey: "kubernetes.io/hostname" @@ -43,13 +43,13 @@ spec: matchLabels: app: cannon containers: - {{- if .Values.service.nginz.enabled }} + {{- if .Values.cannon.service.nginz.enabled }} - name: nginz - image: "{{ .Values.nginzImage.repository }}:{{ .Values.nginzImage.tag }}" - imagePullPolicy: "{{ .Values.nginzImage.pullPolicy }}" + image: "{{ .Values.cannon.nginzImage.repository }}:{{ .Values.cannon.nginzImage.tag }}" + imagePullPolicy: "{{ .Values.cannon.nginzImage.pullPolicy }}" {{- if eq (include "includeSecurityContext" .) "true" }} securityContext: - {{- toYaml .Values.podSecurityContext | nindent 10 }} + {{- toYaml .Values.cannon.podSecurityContext | nindent 10 }} {{- end }} env: # Any file changes to this path causes nginx to reload configs without @@ -68,18 +68,18 @@ spec: readOnly: true ports: - name: https - containerPort: {{ .Values.service.nginz.internalPort }} + containerPort: {{ .Values.cannon.service.nginz.internalPort }} readinessProbe: httpGet: path: /status - port: {{ .Values.service.nginz.internalPort }} + port: {{ .Values.cannon.service.nginz.internalPort }} scheme: HTTPS livenessProbe: initialDelaySeconds: 30 timeoutSeconds: 1 httpGet: path: /status - port: {{ .Values.service.nginz.internalPort }} + port: {{ .Values.cannon.service.nginz.internalPort }} scheme: HTTPS lifecycle: preStop: @@ -88,9 +88,9 @@ spec: # which would cause nginz to exit, breaking existing websocket connections. # Instead we terminate gracefully and sleep given grace period + 5 seconds. # (SIGTERM is still sent, but afterwards) - command: ["sh", "-c", "nginx -c /etc/wire/nginz/conf/nginx.conf -s quit && sleep {{ add .Values.config.drainOpts.gracePeriodSeconds 5 }}"] + command: ["sh", "-c", "nginx -c /etc/wire/nginz/conf/nginx.conf -s quit && sleep {{ add .Values.cannon.config.drainOpts.gracePeriodSeconds 5 }}"] resources: - {{- toYaml .Values.resources | nindent 10 }} + {{- toYaml .Values.cannon.resources | nindent 10 }} {{- end }} - name: cannon env: @@ -104,49 +104,49 @@ spec: secretKeyRef: name: cannon key: rabbitmqPassword - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + image: "{{ .Values.cannon.image.repository }}:{{ .Values.cannon.image.tag }}" {{- if eq (include "includeSecurityContext" .) "true" }} securityContext: - {{- toYaml .Values.podSecurityContext | nindent 10 }} + {{- toYaml .Values.cannon.podSecurityContext | nindent 10 }} {{- end }} args: - {{- toYaml .Values.cannonArgs | nindent 10 }} + {{- toYaml .Values.cannon.cannonArgs | nindent 10 }} volumeMounts: - name: empty mountPath: /etc/wire/cannon/externalHost - name: cannon-config mountPath: /etc/wire/cannon/conf - {{- if .Values.config.rabbitmq.tlsCaSecretRef }} + {{- if .Values.cannon.config.rabbitmq.tlsCaSecretRef }} - name: rabbitmq-ca mountPath: "/etc/wire/cannon/rabbitmq-ca/" {{- end }} - {{- if eq (include "useCassandraTLS" .Values.config) "true" }} + {{- if eq (include "useCassandraTLS" .Values.cannon.config.cassandra) "true" }} - name: "cannon-cassandra" mountPath: "/etc/wire/cannon/cassandra" {{- end }} ports: - name: http - containerPort: {{ .Values.service.internalPort }} + containerPort: {{ .Values.cannon.service.internalPort }} readinessProbe: httpGet: path: /i/status - port: {{ .Values.service.internalPort }} + port: {{ .Values.cannon.service.internalPort }} scheme: HTTP livenessProbe: initialDelaySeconds: 30 timeoutSeconds: 1 httpGet: path: /i/status - port: {{ .Values.service.internalPort }} + port: {{ .Values.cannon.service.internalPort }} scheme: HTTP resources: - {{- toYaml .Values.resources | nindent 10 }} + {{- toYaml .Values.cannon.resources | nindent 10 }} initContainers: - name: cannon-configurator image: alpine:3.21.3 {{- if eq (include "includeSecurityContext" .) "true" }} securityContext: - {{- toYaml .Values.podSecurityContext | nindent 10 }} + {{- toYaml .Values.cannon.podSecurityContext | nindent 10 }} runAsUser: 65534 {{- end }} command: @@ -154,7 +154,7 @@ spec: args: - -c # e.g. cannon-0.cannon.production - - echo "${HOSTNAME}.{{ .Values.service.name }}.{{ .Release.Namespace }}" > /etc/wire/cannon/externalHost/host.txt + - echo "${HOSTNAME}.{{ .Values.cannon.service.name }}.{{ .Release.Namespace }}" > /etc/wire/cannon/externalHost/host.txt volumeMounts: - name: empty mountPath: /etc/wire/cannon/externalHost @@ -166,7 +166,7 @@ spec: name: cannon - name: empty emptyDir: {} - {{- if .Values.service.nginz.enabled }} + {{- if .Values.cannon.service.nginz.enabled }} - name: nginz-config configMap: name: cannon-nginz @@ -175,27 +175,27 @@ spec: secretName: cannon-nginz - name: certificate secret: - secretName: {{ .Values.service.nginz.tls.secretName }} + secretName: {{ .Values.cannon.service.nginz.tls.secretName }} {{- end }} - {{- if .Values.config.rabbitmq.tlsCaSecretRef }} + {{- if .Values.cannon.config.rabbitmq.tlsCaSecretRef }} - name: rabbitmq-ca secret: - secretName: {{ .Values.config.rabbitmq.tlsCaSecretRef.name }} + secretName: {{ .Values.cannon.config.rabbitmq.tlsCaSecretRef.name }} {{- end }} - {{- if eq (include "useCassandraTLS" .Values.config) "true" }} + {{- if eq (include "useCassandraTLS" .Values.cannon.config.cassandra) "true" }} - name: "cannon-cassandra" secret: - secretName: {{ (include "tlsSecretRef" .Values.config | fromYaml).name }} + secretName: {{ (include "cannon.tlsSecretRef" .Values.cannon.config | fromYaml).name }} {{- end }} - {{- with .Values.nodeSelector }} + {{- with .Values.cannon.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} - {{- with .Values.affinity }} + {{- with .Values.cannon.affinity }} affinity: {{- toYaml . | nindent 8 }} {{- end }} - {{- with .Values.tolerations }} + {{- with .Values.cannon.tolerations }} tolerations: {{- toYaml . | nindent 8 }} {{- end }} diff --git a/charts/cargohold/templates/configmap.yaml b/charts/wire-server/templates/cargohold/configmap.yaml similarity index 74% rename from charts/cargohold/templates/configmap.yaml rename to charts/wire-server/templates/cargohold/configmap.yaml index 526335d8f43..dc0a337e461 100644 --- a/charts/cargohold/templates/configmap.yaml +++ b/charts/wire-server/templates/cargohold/configmap.yaml @@ -4,15 +4,15 @@ metadata: name: "cargohold" data: cargohold.yaml: | - logFormat: {{ .Values.config.logFormat }} - logLevel: {{ .Values.config.logLevel }} - logNetStrings: {{ .Values.config.logNetStrings }} + logFormat: {{ .Values.cargohold.config.logFormat }} + logLevel: {{ .Values.cargohold.config.logLevel }} + logNetStrings: {{ .Values.cargohold.config.logNetStrings }} cargohold: host: 0.0.0.0 - port: {{ .Values.service.internalPort }} + port: {{ .Values.cargohold.service.internalPort }} - {{- if .Values.config.enableFederation }} + {{- if $.Values.cargohold.config.enableFederation }} federator: host: federator port: 8080 @@ -23,7 +23,7 @@ data: port: 8080 aws: - {{- with .Values.config.aws }} + {{- with .Values.cargohold.config.aws }} s3Bucket: {{ .s3Bucket }} s3Endpoint: {{ .s3Endpoint }} {{- if .s3DownloadEndpoint }} @@ -48,7 +48,7 @@ data: {{- end }} settings: - {{- with .Values.config.settings }} + {{- with .Values.cargohold.config.settings }} {{- if .maxTotalBytes }} maxTotalBytes: {{ .maxTotalBytes }} {{- end }} @@ -59,6 +59,6 @@ data: downloadLinkTTL: {{ .downloadLinkTTL }} {{- end }} assetAuditLogEnabled: {{ .assetAuditLogEnabled }} - federationDomain: {{ .federationDomain }} + federationDomain: {{ $.Values.cargohold.config.settings.federationDomain }} disabledAPIVersions: {{ toJson .disabledAPIVersions }} {{- end }} diff --git a/charts/cargohold/templates/deployment.yaml b/charts/wire-server/templates/cargohold/deployment.yaml similarity index 74% rename from charts/cargohold/templates/deployment.yaml rename to charts/wire-server/templates/cargohold/deployment.yaml index fe25a506cc2..10fca7259ed 100644 --- a/charts/cargohold/templates/deployment.yaml +++ b/charts/wire-server/templates/cargohold/deployment.yaml @@ -8,12 +8,12 @@ metadata: release: {{ .Release.Name }} heritage: {{ .Release.Service }} spec: - replicas: {{ .Values.replicaCount }} + replicas: {{ .Values.cargohold.replicaCount }} strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 0 - maxSurge: {{ .Values.replicaCount }} + maxSurge: {{ .Values.cargohold.replicaCount }} selector: matchLabels: app: cargohold @@ -24,10 +24,10 @@ spec: release: {{ .Release.Name }} annotations: # An annotation of the configmap checksum ensures changes to the configmap cause a redeployment upon `helm upgrade` - checksum/configmap: {{ include (print .Template.BasePath "/configmap.yaml") . | sha256sum }} - checksum/secret: {{ include (print .Template.BasePath "/secret.yaml") . | sha256sum }} + checksum/configmap: {{ include (print .Template.BasePath "/cargohold/configmap.yaml") . | sha256sum }} + checksum/secret: {{ include (print .Template.BasePath "/cargohold/secret.yaml") . | sha256sum }} spec: - serviceAccountName: {{ .Values.serviceAccount.name }} + serviceAccountName: {{ .Values.cargohold.serviceAccount.name }} topologySpreadConstraints: - maxSkew: 1 topologyKey: "kubernetes.io/hostname" @@ -44,11 +44,11 @@ spec: secretName: "cargohold" containers: - name: cargohold - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" - imagePullPolicy: {{ default "" .Values.imagePullPolicy | quote }} + image: "{{ .Values.cargohold.image.repository }}:{{ .Values.cargohold.image.tag }}" + imagePullPolicy: {{ default "" .Values.cargohold.imagePullPolicy | quote }} {{- if eq (include "includeSecurityContext" .) "true" }} securityContext: - {{- toYaml .Values.podSecurityContext | nindent 12 }} + {{- toYaml .Values.cargohold.podSecurityContext | nindent 12 }} {{- end }} volumeMounts: - name: "cargohold-secrets" @@ -56,7 +56,7 @@ spec: - name: "cargohold-config" mountPath: "/etc/wire/cargohold/conf" env: - {{- if hasKey .Values.secrets "awsKeyId" }} + {{- if hasKey .Values.cargohold.secrets "awsKeyId" }} - name: AWS_ACCESS_KEY_ID valueFrom: secretKeyRef: @@ -69,8 +69,8 @@ spec: key: awsSecretKey {{- end }} - name: AWS_REGION - value: "{{ .Values.config.aws.region }}" - {{- with .Values.config.proxy }} + value: "{{ .Values.cargohold.config.aws.region }}" + {{- with .Values.cargohold.config.proxy }} {{- if .httpProxy }} - name: http_proxy value: {{ .httpProxy | quote }} @@ -91,16 +91,16 @@ spec: {{- end }} {{- end }} ports: - - containerPort: {{ .Values.service.internalPort }} + - containerPort: {{ .Values.cargohold.service.internalPort }} livenessProbe: httpGet: scheme: HTTP path: /i/status - port: {{ .Values.service.internalPort }} + port: {{ .Values.cargohold.service.internalPort }} readinessProbe: httpGet: scheme: HTTP path: /i/status - port: {{ .Values.service.internalPort }} + port: {{ .Values.cargohold.service.internalPort }} resources: -{{ toYaml .Values.resources | indent 12 }} +{{ toYaml .Values.cargohold.resources | indent 12 }} diff --git a/charts/cargohold/templates/poddisruptionbudget.yaml b/charts/wire-server/templates/cargohold/poddisruptionbudget.yaml similarity index 72% rename from charts/cargohold/templates/poddisruptionbudget.yaml rename to charts/wire-server/templates/cargohold/poddisruptionbudget.yaml index be7075ee074..7757bf7cb44 100644 --- a/charts/cargohold/templates/poddisruptionbudget.yaml +++ b/charts/wire-server/templates/cargohold/poddisruptionbudget.yaml @@ -1,4 +1,4 @@ -{{- if gt (int .Values.replicaCount) 1 }} +{{- if gt (int .Values.cargohold.replicaCount) 1 }} apiVersion: policy/v1 kind: PodDisruptionBudget metadata: @@ -9,7 +9,7 @@ metadata: release: {{ .Release.Name }} heritage: {{ .Release.Service }} spec: - maxUnavailable: {{ sub (int .Values.replicaCount) 1 }} + maxUnavailable: {{ sub (int .Values.cargohold.replicaCount) 1 }} selector: matchLabels: app: cargohold diff --git a/charts/cargohold/templates/secret.yaml b/charts/wire-server/templates/cargohold/secret.yaml similarity index 79% rename from charts/cargohold/templates/secret.yaml rename to charts/wire-server/templates/cargohold/secret.yaml index 504d06584d0..b72f4de8621 100644 --- a/charts/cargohold/templates/secret.yaml +++ b/charts/wire-server/templates/cargohold/secret.yaml @@ -9,10 +9,10 @@ metadata: heritage: "{{ .Release.Service }}" type: Opaque data: - {{/* for_helm_linting is necessary only since the 'with' block below does not throw an error upon an empty .Values.secrets */}} - for_helm_linting: {{ required "No .secrets found in configuration. Did you forget to helm -f path/to/secrets.yaml ?" .Values.secrets | quote | b64enc | quote }} + {{/* for_helm_linting is necessary only since the 'with' block below does not throw an error upon an empty .Values.cargohold.secrets */}} + for_helm_linting: {{ required "No .secrets found in configuration. Did you forget to helm -f path/to/secrets.yaml ?" .Values.cargohold.secrets | quote | b64enc | quote }} - {{- with .Values.secrets }} + {{- with .Values.cargohold.secrets }} {{ if .cloudFront }} cf-pk.pem: {{ .cloudFront.cfPrivateKey | b64enc | quote }} {{ end }} diff --git a/charts/cargohold/templates/service.yaml b/charts/wire-server/templates/cargohold/service.yaml similarity index 81% rename from charts/cargohold/templates/service.yaml rename to charts/wire-server/templates/cargohold/service.yaml index 28fd6f8fd38..cc794baac93 100644 --- a/charts/cargohold/templates/service.yaml +++ b/charts/wire-server/templates/cargohold/service.yaml @@ -17,8 +17,8 @@ spec: type: ClusterIP ports: - name: http - port: {{ .Values.service.externalPort }} - targetPort: {{ .Values.service.internalPort }} + port: {{ .Values.cargohold.service.externalPort }} + targetPort: {{ .Values.cargohold.service.internalPort }} selector: app: cargohold release: {{ .Release.Name }} diff --git a/charts/cargohold/templates/serviceaccount.yaml b/charts/wire-server/templates/cargohold/serviceaccount.yaml similarity index 52% rename from charts/cargohold/templates/serviceaccount.yaml rename to charts/wire-server/templates/cargohold/serviceaccount.yaml index 199206e427a..d0b1424f06c 100644 --- a/charts/cargohold/templates/serviceaccount.yaml +++ b/charts/wire-server/templates/cargohold/serviceaccount.yaml @@ -1,16 +1,16 @@ -{{- if .Values.serviceAccount.create -}} +{{- if .Values.cargohold.serviceAccount.create -}} apiVersion: v1 kind: ServiceAccount metadata: - name: {{ .Values.serviceAccount.name }} + name: {{ .Values.cargohold.serviceAccount.name }} labels: app: cargohold chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} - {{- with .Values.serviceAccount.annotations }} + {{- with .Values.cargohold.serviceAccount.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} -automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} +automountServiceAccountToken: {{ .Values.cargohold.serviceAccount.automountServiceAccountToken }} {{- end }} diff --git a/charts/cargohold/templates/servicemonitor.yaml b/charts/wire-server/templates/cargohold/servicemonitor.yaml similarity index 87% rename from charts/cargohold/templates/servicemonitor.yaml rename to charts/wire-server/templates/cargohold/servicemonitor.yaml index 106fad9ff31..480e6522dcc 100644 --- a/charts/cargohold/templates/servicemonitor.yaml +++ b/charts/wire-server/templates/cargohold/servicemonitor.yaml @@ -1,4 +1,4 @@ -{{- if .Values.metrics.serviceMonitor.enabled }} +{{- if .Values.cargohold.metrics.serviceMonitor.enabled }} apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: diff --git a/charts/cargohold/templates/tests/cargohold-integration.yaml b/charts/wire-server/templates/cargohold/tests/cargohold-integration.yaml similarity index 83% rename from charts/cargohold/templates/tests/cargohold-integration.yaml rename to charts/wire-server/templates/cargohold/tests/cargohold-integration.yaml index 170e4e02595..df6518e0bbe 100644 --- a/charts/cargohold/templates/tests/cargohold-integration.yaml +++ b/charts/wire-server/templates/cargohold/tests/cargohold-integration.yaml @@ -16,10 +16,10 @@ spec: # NOTE: the bucket for these tests must be created. # If using the wire-server/fake-aws-s3 chart, `dummy-bucket` will already be created. - name: integration - image: "{{ .Values.image.repository }}-integration:{{ .Values.image.tag }}" + image: "{{ .Values.cargohold.image.repository }}-integration:{{ .Values.cargohold.image.tag }}" {{- if eq (include "includeSecurityContext" .) "true" }} securityContext: - {{- toYaml .Values.podSecurityContext | nindent 6 }} + {{- toYaml .Values.cargohold.podSecurityContext | nindent 6 }} {{- end }} command: - /bin/bash @@ -33,7 +33,7 @@ spec: exit_code=$? fi - {{- if .Values.tests.config.uploadXml }} + {{- if .Values.cargohold.tests.config.uploadXml }} # In case a different S3 compliant storage is used to upload test result. if ! [[ -z "${UPLOAD_XML_AWS_ACCESS_KEY_ID+x}" ]]; then export AWS_ACCESS_KEY_ID="$UPLOAD_XML_AWS_ACCESS_KEY_ID" @@ -66,13 +66,13 @@ spec: name: cargohold key: awsSecretKey - name: AWS_REGION - value: "{{ .Values.config.aws.region }}" + value: "{{ .Values.cargohold.config.aws.region }}" - name: TEST_XML value: /tmp/result.xml - {{- if .Values.tests.config.uploadXml }} + {{- if .Values.cargohold.tests.config.uploadXml }} - name: UPLOAD_XML_S3_BASE_URL - value: {{ .Values.tests.config.uploadXml.baseUrl }} - {{- if .Values.tests.secrets.uploadXmlAwsAccessKeyId }} + value: {{ .Values.cargohold.tests.config.uploadXml.baseUrl }} + {{- if .Values.cargohold.tests.secrets.uploadXmlAwsAccessKeyId }} - name: UPLOAD_XML_AWS_ACCESS_KEY_ID valueFrom: secretKeyRef: diff --git a/charts/cargohold/templates/tests/configmap.yaml b/charts/wire-server/templates/cargohold/tests/configmap.yaml similarity index 72% rename from charts/cargohold/templates/tests/configmap.yaml rename to charts/wire-server/templates/cargohold/tests/configmap.yaml index aa5a8aa4a19..1542a14aa06 100644 --- a/charts/cargohold/templates/tests/configmap.yaml +++ b/charts/wire-server/templates/cargohold/tests/configmap.yaml @@ -9,12 +9,12 @@ data: integration.yaml: | cargohold: host: cargohold - port: {{ .Values.service.internalPort }} - {{- if .Values.config.enableFederation }} + port: {{ .Values.cargohold.service.internalPort }} + {{- if .Values.cargohold.config.enableFederation }} federator: host: federator port: 8080 {{- end }} brig: host: brig - port: 8080 \ No newline at end of file + port: 8080 diff --git a/charts/cargohold/templates/tests/secret.yaml b/charts/wire-server/templates/cargohold/tests/secret.yaml similarity index 90% rename from charts/cargohold/templates/tests/secret.yaml rename to charts/wire-server/templates/cargohold/tests/secret.yaml index a8bd1f0ae63..b18334564e1 100644 --- a/charts/cargohold/templates/tests/secret.yaml +++ b/charts/wire-server/templates/cargohold/tests/secret.yaml @@ -9,7 +9,7 @@ metadata: heritage: "{{ .Release.Service }}" type: Opaque data: - {{- with .Values.tests.secrets }} + {{- with .Values.cargohold.tests.secrets }} uploadXmlAwsAccessKeyId: {{ .uploadXmlAwsAccessKeyId | b64enc | quote }} uploadXmlAwsSecretAccessKey: {{ .uploadXmlAwsSecretAccessKey | b64enc | quote }} {{- end }} diff --git a/charts/galley/templates/aws-secret.yaml b/charts/wire-server/templates/galley/aws-secret.yaml similarity index 79% rename from charts/galley/templates/aws-secret.yaml rename to charts/wire-server/templates/galley/aws-secret.yaml index a72862ee5a4..7d8db2ee352 100644 --- a/charts/galley/templates/aws-secret.yaml +++ b/charts/wire-server/templates/galley/aws-secret.yaml @@ -1,4 +1,4 @@ -{{- if hasKey .Values.secrets "awsKeyId" }} +{{- if hasKey .Values.galley.secrets "awsKeyId" }} apiVersion: v1 kind: Secret metadata: @@ -10,7 +10,7 @@ metadata: heritage: "{{ .Release.Service }}" type: Opaque data: - {{- with .Values.secrets }} + {{- with .Values.galley.secrets }} awsKeyId: {{ .awsKeyId | b64enc | quote }} awsSecretKey: {{ .awsSecretKey | b64enc | quote }} {{- end }} diff --git a/charts/galley/templates/cassandra-secret.yaml b/charts/wire-server/templates/galley/cassandra-secret.yaml similarity index 70% rename from charts/galley/templates/cassandra-secret.yaml rename to charts/wire-server/templates/galley/cassandra-secret.yaml index eb34aeb30bd..93aaa97db81 100644 --- a/charts/galley/templates/cassandra-secret.yaml +++ b/charts/wire-server/templates/galley/cassandra-secret.yaml @@ -1,5 +1,5 @@ {{/* Secret for the provided Cassandra TLS CA. */}} -{{- if not (empty .Values.config.cassandra.tlsCa) }} +{{- if not (empty .Values.galley.config.cassandra.tlsCa) }} apiVersion: v1 kind: Secret metadata: @@ -11,5 +11,5 @@ metadata: heritage: "{{ .Release.Service }}" type: Opaque data: - ca.pem: {{ .Values.config.cassandra.tlsCa | b64enc | quote }} + ca.pem: {{ .Values.galley.config.cassandra.tlsCa | b64enc | quote }} {{- end }} diff --git a/charts/galley/templates/configmap.yaml b/charts/wire-server/templates/galley/configmap.yaml similarity index 91% rename from charts/galley/templates/configmap.yaml rename to charts/wire-server/templates/galley/configmap.yaml index 77544219168..1afe5e88786 100644 --- a/charts/galley/templates/configmap.yaml +++ b/charts/wire-server/templates/galley/configmap.yaml @@ -3,7 +3,7 @@ kind: ConfigMap metadata: name: galley data: - {{- with .Values.config }} + {{- with .Values.galley.config }} galley.yaml: | logFormat: {{ .logFormat }} logLevel: {{ .logLevel }} @@ -21,13 +21,13 @@ data: {{- if hasKey .cassandra "filterNodesByDatacentre" }} filterNodesByDatacentre: {{ .cassandra.filterNodesByDatacentre }} {{- end }} - {{- if eq (include "useCassandraTLS" .) "true" }} - tlsCa: /etc/wire/galley/cassandra/{{- (include "tlsSecretRef" . | fromYaml).key }} + {{- if eq (include "useCassandraTLS" .cassandra) "true" }} + tlsCa: /etc/wire/galley/cassandra/{{- (include "galley.tlsSecretRef" . | fromYaml).key }} {{- end }} postgresql: {{ toYaml .postgresql | nindent 6 }} postgresqlPool: {{ toYaml .postgresqlPool | nindent 6 }} - {{- if hasKey $.Values.secrets "pgPassword" }} + {{- if hasKey $.Values.galley.secrets "pgPassword" }} postgresqlPassword: /etc/wire/galley/secrets/pgPassword {{- end }} @@ -43,7 +43,7 @@ data: host: spar port: 8080 - {{- if .enableFederation }} + {{- if $.Values.galley.config.enableFederation }} federator: host: federator port: 8080 @@ -90,11 +90,11 @@ data: {{- if (and .settings.conversationCodeURI .settings.multiIngress) }} {{ fail "settings.conversationCodeURI and settings.multiIngress are mutually exclusive" }} {{- end }} - federationDomain: {{ .settings.federationDomain }} + federationDomain: {{ $.Values.galley.config.settings.federationDomain }} {{- if .settings.federationProtocols }} federationProtocols: {{ .settings.federationProtocols }} {{- end }} - {{- if $.Values.secrets.mlsPrivateKeys }} + {{- if $.Values.galley.secrets.mlsPrivateKeys }} mlsPrivateKeyPaths: removal: ed25519: "/etc/wire/galley/secrets/removal_ed25519.pem" @@ -136,9 +136,11 @@ data: searchVisibilityInbound: {{- toYaml .settings.featureFlags.searchVisibilityInbound | nindent 10 }} {{- end }} - {{- if .settings.featureFlags.validateSAMLEmails }} - validateSAMLEmails: - {{- toYaml .settings.featureFlags.validateSAMLEmails | nindent 10 }} + {{- /* Accept the legacy typo in Helm values, but always render the canonical Galley key. */}} + {{- $validateSAMLemails := .settings.featureFlags.validateSAMLemails | default .settings.featureFlags.validateSAMLEmails }} + {{- if $validateSAMLemails }} + validateSAMLemails: + {{- toYaml $validateSAMLemails | nindent 10 }} {{- end }} {{- if .settings.featureFlags.appLock }} appLock: diff --git a/charts/galley/templates/deployment.yaml b/charts/wire-server/templates/galley/deployment.yaml similarity index 72% rename from charts/galley/templates/deployment.yaml rename to charts/wire-server/templates/galley/deployment.yaml index 5bb13ed58b9..e95638c2dd1 100644 --- a/charts/galley/templates/deployment.yaml +++ b/charts/wire-server/templates/galley/deployment.yaml @@ -8,12 +8,12 @@ metadata: release: {{ .Release.Name }} heritage: {{ .Release.Service }} spec: - replicas: {{ .Values.replicaCount }} + replicas: {{ .Values.galley.replicaCount }} strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 0 - maxSurge: {{ .Values.replicaCount }} + maxSurge: {{ .Values.galley.replicaCount }} selector: matchLabels: app: galley @@ -24,9 +24,9 @@ spec: release: {{ .Release.Name }} annotations: # An annotation of the configmap checksum ensures changes to the configmap cause a redeployment upon `helm upgrade` - checksum/configmap: {{ include (print .Template.BasePath "/configmap.yaml") . | sha256sum }} - checksum/aws-secret: {{ include (print .Template.BasePath "/aws-secret.yaml") . | sha256sum }} - checksum/secret: {{ include (print .Template.BasePath "/secret.yaml") . | sha256sum }} + checksum/configmap: {{ include (print .Template.BasePath "/galley/configmap.yaml") . | sha256sum }} + checksum/aws-secret: {{ include (print .Template.BasePath "/galley/aws-secret.yaml") . | sha256sum }} + checksum/secret: {{ include (print .Template.BasePath "/galley/secret.yaml") . | sha256sum }} spec: topologySpreadConstraints: - maxSkew: 1 @@ -35,7 +35,7 @@ spec: labelSelector: matchLabels: app: galley - serviceAccountName: {{ .Values.serviceAccount.name }} + serviceAccountName: {{ .Values.galley.serviceAccount.name }} volumes: - name: "galley-config" configMap: @@ -43,37 +43,37 @@ spec: - name: "galley-secrets" secret: secretName: "galley" - {{- if eq (include "useCassandraTLS" .Values.config) "true" }} + {{- if eq (include "useCassandraTLS" .Values.galley.config.cassandra) "true" }} - name: "galley-cassandra" secret: - secretName: {{ (include "tlsSecretRef" .Values.config | fromYaml).name }} + secretName: {{ (include "galley.tlsSecretRef" .Values.galley.config | fromYaml).name }} {{- end }} - {{- if .Values.config.rabbitmq.tlsCaSecretRef }} + {{- if .Values.galley.config.rabbitmq.tlsCaSecretRef }} - name: "rabbitmq-ca" secret: - secretName: {{ .Values.config.rabbitmq.tlsCaSecretRef.name }} + secretName: {{ .Values.galley.config.rabbitmq.tlsCaSecretRef.name }} {{- end }} {{- if .Values.additionalVolumes }} {{ toYaml .Values.additionalVolumes | nindent 8 }} {{- end }} containers: - name: galley - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" - imagePullPolicy: {{ default "" .Values.imagePullPolicy | quote }} + image: "{{ .Values.galley.image.repository }}:{{ .Values.galley.image.tag }}" + imagePullPolicy: {{ default "" .Values.galley.imagePullPolicy | quote }} {{- if eq (include "includeSecurityContext" .) "true" }} securityContext: - {{- toYaml .Values.podSecurityContext | nindent 12 }} + {{- toYaml .Values.galley.podSecurityContext | nindent 12 }} {{- end }} volumeMounts: - name: "galley-config" mountPath: "/etc/wire/galley/conf" - name: "galley-secrets" mountPath: "/etc/wire/galley/secrets" - {{- if eq (include "useCassandraTLS" .Values.config) "true" }} + {{- if eq (include "useCassandraTLS" .Values.galley.config.cassandra) "true" }} - name: "galley-cassandra" mountPath: "/etc/wire/galley/cassandra" {{- end }} - {{- if .Values.config.rabbitmq.tlsCaSecretRef }} + {{- if .Values.galley.config.rabbitmq.tlsCaSecretRef }} - name: "rabbitmq-ca" mountPath: "/etc/wire/galley/rabbitmq-ca/" {{- end }} @@ -81,7 +81,7 @@ spec: {{ toYaml .Values.additionalVolumeMounts | nindent 10 }} {{- end }} env: - {{- if hasKey .Values.secrets "awsKeyId" }} + {{- if hasKey .Values.galley.secrets "awsKeyId" }} - name: AWS_ACCESS_KEY_ID valueFrom: secretKeyRef: @@ -94,8 +94,8 @@ spec: key: awsSecretKey {{- end }} - name: AWS_REGION - value: "{{ .Values.config.aws.region }}" - {{- with .Values.config.proxy }} + value: "{{ .Values.galley.config.aws.region }}" + {{- with .Values.galley.config.proxy }} {{- if .httpProxy }} - name: http_proxy value: {{ .httpProxy | quote }} @@ -115,7 +115,7 @@ spec: value: {{ join "," .noProxyList | quote }} {{- end }} {{- end }} - {{- if .Values.config.enableFederation }} + {{- if .Values.galley.config.enableFederation }} - name: RABBITMQ_USERNAME valueFrom: secretKeyRef: @@ -128,21 +128,21 @@ spec: key: rabbitmqPassword {{- end }} ports: - - containerPort: {{ .Values.service.internalPort }} + - containerPort: {{ .Values.galley.service.internalPort }} livenessProbe: httpGet: scheme: HTTP path: /i/status - port: {{ .Values.service.internalPort }} + port: {{ .Values.galley.service.internalPort }} readinessProbe: httpGet: scheme: HTTP path: /i/status - port: {{ .Values.service.internalPort }} + port: {{ .Values.galley.service.internalPort }} {{- if .Values.preStop }} lifecycle: preStop: {{ toYaml .Values.preStop | indent 14 }} {{- end }} resources: -{{ toYaml .Values.resources | indent 12 }} +{{ toYaml .Values.galley.resources | indent 12 }} diff --git a/charts/galley/templates/poddisruptionbudget.yaml b/charts/wire-server/templates/galley/poddisruptionbudget.yaml similarity index 72% rename from charts/galley/templates/poddisruptionbudget.yaml rename to charts/wire-server/templates/galley/poddisruptionbudget.yaml index c838166c0c3..83f1ae197e1 100644 --- a/charts/galley/templates/poddisruptionbudget.yaml +++ b/charts/wire-server/templates/galley/poddisruptionbudget.yaml @@ -1,4 +1,4 @@ -{{- if gt (int .Values.replicaCount) 1 }} +{{- if gt (int .Values.galley.replicaCount) 1 }} apiVersion: policy/v1 kind: PodDisruptionBudget metadata: @@ -9,7 +9,7 @@ metadata: release: {{ .Release.Name }} heritage: {{ .Release.Service }} spec: - maxUnavailable: {{ sub (int .Values.replicaCount) 1 }} + maxUnavailable: {{ sub (int .Values.galley.replicaCount) 1 }} selector: matchLabels: app: galley diff --git a/charts/wire-server/templates/galley/secret.yaml b/charts/wire-server/templates/galley/secret.yaml new file mode 100644 index 00000000000..8425394ddd4 --- /dev/null +++ b/charts/wire-server/templates/galley/secret.yaml @@ -0,0 +1,29 @@ +apiVersion: v1 +kind: Secret +metadata: + name: galley + labels: + app: galley + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +type: Opaque +data: + {{- if .Values.galley.secrets.mlsPrivateKeys }} + removal_ed25519.pem: {{ .Values.galley.secrets.mlsPrivateKeys.removal.ed25519 | b64enc | quote }} + removal_ecdsa_secp256r1_sha256.pem: {{ .Values.galley.secrets.mlsPrivateKeys.removal.ecdsa_secp256r1_sha256 | b64enc | quote }} + removal_ecdsa_secp384r1_sha384.pem: {{ .Values.galley.secrets.mlsPrivateKeys.removal.ecdsa_secp384r1_sha384 | b64enc | quote }} + removal_ecdsa_secp521r1_sha512.pem: {{ .Values.galley.secrets.mlsPrivateKeys.removal.ecdsa_secp521r1_sha512 | b64enc | quote }} + {{- end -}} + + {{- if $.Values.galley.config.enableFederation }} + {{- if not $.Values.galley.secrets.rabbitmq }} + {{- fail "galley.secrets.rabbitmq must be configured when enableFederation is true" }} + {{- end }} + rabbitmqUsername: {{ .Values.galley.secrets.rabbitmq.username | b64enc | quote }} + rabbitmqPassword: {{ .Values.galley.secrets.rabbitmq.password | b64enc | quote }} + {{- end }} + + {{- if .Values.galley.secrets.pgPassword }} + pgPassword: {{ .Values.galley.secrets.pgPassword | b64enc | quote }} + {{- end }} diff --git a/charts/galley/templates/service.yaml b/charts/wire-server/templates/galley/service.yaml similarity index 82% rename from charts/galley/templates/service.yaml rename to charts/wire-server/templates/galley/service.yaml index 3e401047911..2dc2112eabf 100644 --- a/charts/galley/templates/service.yaml +++ b/charts/wire-server/templates/galley/service.yaml @@ -17,8 +17,8 @@ spec: type: ClusterIP ports: - name: http - port: {{ .Values.service.externalPort }} - targetPort: {{ .Values.service.internalPort }} + port: {{ .Values.galley.service.externalPort }} + targetPort: {{ .Values.galley.service.internalPort }} selector: app: galley release: {{ .Release.Name }} diff --git a/charts/galley/templates/serviceaccount.yaml b/charts/wire-server/templates/galley/serviceaccount.yaml similarity index 53% rename from charts/galley/templates/serviceaccount.yaml rename to charts/wire-server/templates/galley/serviceaccount.yaml index 29b763c398e..6ba3b03fe62 100644 --- a/charts/galley/templates/serviceaccount.yaml +++ b/charts/wire-server/templates/galley/serviceaccount.yaml @@ -1,16 +1,16 @@ -{{- if .Values.serviceAccount.create -}} +{{- if .Values.galley.serviceAccount.create -}} apiVersion: v1 kind: ServiceAccount metadata: - name: {{ .Values.serviceAccount.name }} + name: {{ .Values.galley.serviceAccount.name }} labels: app: galley chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} - {{- with .Values.serviceAccount.annotations }} + {{- with .Values.galley.serviceAccount.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} -automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} +automountServiceAccountToken: {{ .Values.galley.serviceAccount.automountServiceAccountToken }} {{- end }} diff --git a/charts/galley/templates/servicemonitor.yaml b/charts/wire-server/templates/galley/servicemonitor.yaml similarity index 87% rename from charts/galley/templates/servicemonitor.yaml rename to charts/wire-server/templates/galley/servicemonitor.yaml index 8d9e43f8e51..a88e4358e57 100644 --- a/charts/galley/templates/servicemonitor.yaml +++ b/charts/wire-server/templates/galley/servicemonitor.yaml @@ -1,4 +1,4 @@ -{{- if .Values.metrics.serviceMonitor.enabled }} +{{- if .Values.galley.metrics.serviceMonitor.enabled }} apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: diff --git a/charts/galley/templates/tests/configmap.yaml b/charts/wire-server/templates/galley/tests/configmap.yaml similarity index 92% rename from charts/galley/templates/tests/configmap.yaml rename to charts/wire-server/templates/galley/tests/configmap.yaml index 89d7d589de5..86be4122364 100644 --- a/charts/galley/templates/tests/configmap.yaml +++ b/charts/wire-server/templates/galley/tests/configmap.yaml @@ -9,7 +9,7 @@ data: integration.yaml: | galley: host: galley - port: {{ .Values.service.internalPort }} + port: {{ .Values.galley.service.internalPort }} brig: host: brig diff --git a/charts/galley/templates/tests/galley-integration.yaml b/charts/wire-server/templates/galley/tests/galley-integration.yaml similarity index 77% rename from charts/galley/templates/tests/galley-integration.yaml rename to charts/wire-server/templates/galley/tests/galley-integration.yaml index b7f71d353e6..9256af1db6a 100644 --- a/charts/galley/templates/tests/galley-integration.yaml +++ b/charts/wire-server/templates/galley/tests/galley-integration.yaml @@ -40,23 +40,23 @@ spec: - name: "galley-secrets" secret: secretName: "galley" - {{- if eq (include "useCassandraTLS" .Values.config) "true" }} + {{- if eq (include "useCassandraTLS" .Values.galley.config.cassandra) "true" }} - name: "galley-cassandra" secret: - secretName: {{ (include "tlsSecretRef" .Values.config | fromYaml).name }} + secretName: {{ (include "galley.tlsSecretRef" .Values.galley.config | fromYaml).name }} {{- end }} - {{- if .Values.config.rabbitmq.tlsCaSecretRef }} + {{- if .Values.galley.config.rabbitmq.tlsCaSecretRef }} - name: "rabbitmq-ca" secret: - secretName: {{ .Values.config.rabbitmq.tlsCaSecretRef.name }} + secretName: {{ .Values.galley.config.rabbitmq.tlsCaSecretRef.name }} {{- end }} containers: - name: integration - image: "{{ .Values.image.repository }}-integration:{{ .Values.image.tag }}" - {{- if eq (include "includeSecurityContext" .) "true" }} + image: "{{ .Values.galley.image.repository }}-integration:{{ .Values.galley.image.tag }}" + {{- if eq (include "includeSecurityContext" .) "true" }} securityContext: - {{- toYaml .Values.podSecurityContext | nindent 6 }} - {{- end }} + {{- toYaml .Values.galley.podSecurityContext | nindent 6 }} + {{- end }} command: - /bin/bash - -c @@ -69,7 +69,7 @@ spec: exit_code=$? fi - {{- if .Values.tests.config.uploadXml }} + {{- if .Values.galley.tests.config.uploadXml }} # In case a different S3 compliant storage is used to upload test result. if ! [[ -z "${UPLOAD_XML_AWS_ACCESS_KEY_ID+x}" ]]; then export AWS_ACCESS_KEY_ID="$UPLOAD_XML_AWS_ACCESS_KEY_ID" @@ -94,11 +94,11 @@ spec: mountPath: "/etc/wire/integration-secrets" - name: "galley-secrets" mountPath: "/etc/wire/galley/secrets" - {{- if eq (include "useCassandraTLS" .Values.config) "true" }} + {{- if eq (include "useCassandraTLS" .Values.galley.config.cassandra) "true" }} - name: "galley-cassandra" mountPath: "/etc/wire/galley/cassandra" {{- end }} - {{- if .Values.config.rabbitmq.tlsCaSecretRef }} + {{- if .Values.galley.config.rabbitmq.tlsCaSecretRef }} - name: "rabbitmq-ca" mountPath: "/etc/wire/galley/rabbitmq-ca/" {{- end }} @@ -110,7 +110,7 @@ spec: value: "dummy" - name: AWS_REGION value: "eu-west-1" - {{- if .Values.config.enableFederation }} + {{- if .Values.galley.config.enableFederation }} - name: RABBITMQ_USERNAME value: "guest" - name: RABBITMQ_PASSWORD @@ -118,10 +118,10 @@ spec: {{- end }} - name: TEST_XML value: /tmp/result.xml - {{- if .Values.tests.config.uploadXml }} + {{- if .Values.galley.tests.config.uploadXml }} - name: UPLOAD_XML_S3_BASE_URL - value: {{ .Values.tests.config.uploadXml.baseUrl }} - {{- if .Values.tests.secrets.uploadXmlAwsAccessKeyId }} + value: {{ .Values.galley.tests.config.uploadXml.baseUrl }} + {{- if .Values.galley.tests.secrets.uploadXmlAwsAccessKeyId }} - name: UPLOAD_XML_AWS_ACCESS_KEY_ID valueFrom: secretKeyRef: diff --git a/charts/galley/templates/tests/secret.yaml b/charts/wire-server/templates/galley/tests/secret.yaml similarity index 94% rename from charts/galley/templates/tests/secret.yaml rename to charts/wire-server/templates/galley/tests/secret.yaml index d41373a7f21..b204dd4eddc 100644 --- a/charts/galley/templates/tests/secret.yaml +++ b/charts/wire-server/templates/galley/tests/secret.yaml @@ -12,7 +12,7 @@ metadata: "helm.sh/hook-delete-policy": before-hook-creation type: Opaque data: - {{- with .Values.tests.secrets }} + {{- with .Values.galley.tests.secrets }} provider-privatekey.pem: {{ .providerPrivateKey | b64enc | quote }} provider-publickey.pem: {{ .providerPublicKey | b64enc | quote }} provider-publiccert.pem: {{ .providerPublicCert | b64enc | quote }} diff --git a/charts/gundeck/templates/cassandra-secret.yaml b/charts/wire-server/templates/gundeck/cassandra-secret.yaml similarity index 70% rename from charts/gundeck/templates/cassandra-secret.yaml rename to charts/wire-server/templates/gundeck/cassandra-secret.yaml index 68dd7c9d34a..20805a04937 100644 --- a/charts/gundeck/templates/cassandra-secret.yaml +++ b/charts/wire-server/templates/gundeck/cassandra-secret.yaml @@ -1,5 +1,5 @@ {{/* Secret for the provided Cassandra TLS CA. */}} -{{- if not (empty .Values.config.cassandra.tlsCa) }} +{{- if not (empty .Values.gundeck.config.cassandra.tlsCa) }} apiVersion: v1 kind: Secret metadata: @@ -11,5 +11,5 @@ metadata: heritage: "{{ .Release.Service }}" type: Opaque data: - ca.pem: {{ .Values.config.cassandra.tlsCa | b64enc | quote }} + ca.pem: {{ .Values.gundeck.config.cassandra.tlsCa | b64enc | quote }} {{- end }} diff --git a/charts/gundeck/templates/configmap.yaml b/charts/wire-server/templates/gundeck/configmap.yaml similarity index 82% rename from charts/gundeck/templates/configmap.yaml rename to charts/wire-server/templates/gundeck/configmap.yaml index 26b04fc9337..9f102742700 100644 --- a/charts/gundeck/templates/configmap.yaml +++ b/charts/wire-server/templates/gundeck/configmap.yaml @@ -3,7 +3,7 @@ kind: ConfigMap metadata: name: "gundeck" data: - {{- with .Values.config }} + {{- with .Values.gundeck.config }} gundeck.yaml: | logFormat: {{ .logFormat }} logLevel: {{ .logLevel }} @@ -11,7 +11,7 @@ data: gundeck: host: 0.0.0.0 - port: {{ $.Values.service.internalPort }} + port: {{ $.Values.gundeck.service.internalPort }} brig: host: brig @@ -25,8 +25,8 @@ data: {{- if hasKey .cassandra "filterNodesByDatacentre" }} filterNodesByDatacentre: {{ .cassandra.filterNodesByDatacentre }} {{- end }} - {{- if eq (include "useCassandraTLS" .) "true" }} - tlsCa: /etc/wire/gundeck/cassandra/{{- (include "tlsSecretRef" . | fromYaml).key }} + {{- if eq (include "useCassandraTLS" .cassandra) "true" }} + tlsCa: /etc/wire/gundeck/cassandra/{{- (include "gundeck.tlsSecretRef" . | fromYaml).key }} {{- end }} {{- with .rabbitmq }} @@ -47,8 +47,8 @@ data: connectionMode: {{ .redis.connectionMode }} enableTls: {{ .redis.enableTls }} insecureSkipVerifyTls: {{ .redis.insecureSkipVerifyTls }} - {{- if eq (include "configureRedisCa" .) "true" }} - tlsCa: /etc/wire/gundeck/redis-ca/{{ include "redisTlsSecretKey" .}} + {{- if eq (include "gundeck.configureRedisCa" .) "true" }} + tlsCa: /etc/wire/gundeck/redis-ca/{{ include "gundeck.redisTlsSecretKey" .}} {{- end }} {{- if .redisAdditionalWrite }} @@ -58,8 +58,8 @@ data: connectionMode: {{ .redisAdditionalWrite.connectionMode }} enableTls: {{ .redisAdditionalWrite.enableTls }} insecureSkipVerifyTls: {{ .redisAdditionalWrite.insecureSkipVerifyTls }} - {{- if eq (include "configureAdditionalRedisCa" .) "true" }} - tlsCa: /etc/wire/gundeck/additional-redis-ca/{{ include "additionalRedisTlsSecretKey" .}} + {{- if eq (include "gundeck.configureAdditionalRedisCa" .) "true" }} + tlsCa: /etc/wire/gundeck/additional-redis-ca/{{ include "gundeck.additionalRedisTlsSecretKey" .}} {{- end }} {{- end }} @@ -96,5 +96,6 @@ data: {{- if hasKey . "cellsEventQueue" }} cellsEventQueue: {{ .cellsEventQueue }} {{- end }} + consumableNotifications: false {{- end }} diff --git a/charts/gundeck/templates/deployment.yaml b/charts/wire-server/templates/gundeck/deployment.yaml similarity index 64% rename from charts/gundeck/templates/deployment.yaml rename to charts/wire-server/templates/gundeck/deployment.yaml index c70ddec4d1d..b7d677c88c7 100644 --- a/charts/gundeck/templates/deployment.yaml +++ b/charts/wire-server/templates/gundeck/deployment.yaml @@ -8,12 +8,12 @@ metadata: release: {{ .Release.Name }} heritage: {{ .Release.Service }} spec: - replicas: {{ .Values.replicaCount }} + replicas: {{ .Values.gundeck.replicaCount }} strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 0 - maxSurge: {{ .Values.replicaCount }} + maxSurge: {{ .Values.gundeck.replicaCount }} selector: matchLabels: app: gundeck @@ -24,11 +24,11 @@ spec: release: {{ .Release.Name }} annotations: # An annotation of the configmap checksum ensures changes to the configmap cause a redeployment upon `helm upgrade` - checksum/configmap: {{ include (print .Template.BasePath "/configmap.yaml") . | sha256sum }} - checksum/secret: {{ include (print .Template.BasePath "/secret.yaml") . | sha256sum }} + checksum/configmap: {{ include (print .Template.BasePath "/gundeck/configmap.yaml") . | sha256sum }} + checksum/secret: {{ include (print .Template.BasePath "/gundeck/secret.yaml") . | sha256sum }} spec: - terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }} - serviceAccountName: {{ .Values.serviceAccount.name }} + terminationGracePeriodSeconds: {{ .Values.gundeck.terminationGracePeriodSeconds }} + serviceAccountName: {{ .Values.gundeck.serviceAccount.name }} topologySpreadConstraints: - maxSkew: 1 topologyKey: "kubernetes.io/hostname" @@ -40,50 +40,50 @@ spec: - name: "gundeck-config" configMap: name: "gundeck" - {{- if .Values.config.rabbitmq.tlsCaSecretRef }} + {{- if and .Values.gundeck.config.rabbitmq .Values.gundeck.config.rabbitmq.tlsCaSecretRef }} - name: "rabbitmq-ca" secret: - secretName: {{ .Values.config.rabbitmq.tlsCaSecretRef.name }} + secretName: {{ .Values.gundeck.config.rabbitmq.tlsCaSecretRef.name }} {{- end }} - {{- if eq (include "useCassandraTLS" .Values.config) "true" }} + {{- if eq (include "useCassandraTLS" .Values.gundeck.config.cassandra) "true" }} - name: "gundeck-cassandra" secret: - secretName: {{ (include "tlsSecretRef" .Values.config | fromYaml).name }} + secretName: {{ (include "gundeck.tlsSecretRef" .Values.gundeck.config | fromYaml).name }} {{- end}} - {{- if eq (include "configureRedisCa" .Values.config) "true" }} + {{- if eq (include "gundeck.configureRedisCa" .Values.gundeck.config) "true" }} - name: "redis-ca" secret: - secretName: {{ include "redisTlsSecretName" .Values.config }} + secretName: {{ include "gundeck.redisTlsSecretName" .Values.gundeck.config }} {{- end }} - {{- if eq (include "configureAdditionalRedisCa" .Values.config) "true" }} + {{- if eq (include "gundeck.configureAdditionalRedisCa" .Values.gundeck.config) "true" }} - name: "additional-redis-ca" secret: - secretName: {{ include "additionalRedisTlsSecretName" .Values.config }} + secretName: {{ include "gundeck.additionalRedisTlsSecretName" .Values.gundeck.config }} {{- end }} containers: - name: gundeck - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" - imagePullPolicy: {{ default "" .Values.imagePullPolicy | quote }} + image: "{{ .Values.gundeck.image.repository }}:{{ .Values.gundeck.image.tag }}" + imagePullPolicy: {{ default "" .Values.gundeck.imagePullPolicy | quote }} {{- if eq (include "includeSecurityContext" .) "true" }} securityContext: - {{- toYaml .Values.podSecurityContext | nindent 12 }} + {{- toYaml .Values.gundeck.podSecurityContext | nindent 12 }} {{- end }} volumeMounts: - name: "gundeck-config" mountPath: "/etc/wire/gundeck/conf" - {{- if eq (include "useCassandraTLS" .Values.config) "true" }} + {{- if eq (include "useCassandraTLS" .Values.gundeck.config.cassandra) "true" }} - name: "gundeck-cassandra" mountPath: "/etc/wire/gundeck/cassandra" {{- end }} - {{- if eq (include "configureRedisCa" .Values.config) "true" }} + {{- if eq (include "gundeck.configureRedisCa" .Values.gundeck.config) "true" }} - name: "redis-ca" mountPath: "/etc/wire/gundeck/redis-ca/" {{- end }} - {{- if eq (include "configureAdditionalRedisCa" .Values.config) "true" }} + {{- if eq (include "gundeck.configureAdditionalRedisCa" .Values.gundeck.config) "true" }} - name: "additional-redis-ca" mountPath: "/etc/wire/gundeck/additional-redis-ca/" {{- end }} - {{- if .Values.config.rabbitmq.tlsCaSecretRef }} + {{- if and .Values.gundeck.config.rabbitmq .Values.gundeck.config.rabbitmq.tlsCaSecretRef }} - name: "rabbitmq-ca" mountPath: "/etc/wire/gundeck/rabbitmq-ca/" {{- end }} @@ -98,7 +98,7 @@ spec: secretKeyRef: name: gundeck key: rabbitmqPassword - {{- if hasKey .Values.secrets "awsKeyId" }} + {{- if hasKey .Values.gundeck.secrets "awsKeyId" }} - name: AWS_ACCESS_KEY_ID valueFrom: secretKeyRef: @@ -110,28 +110,28 @@ spec: name: gundeck key: awsSecretKey {{- end }} - {{- if hasKey .Values.secrets "redisUsername" }} + {{- if hasKey .Values.gundeck.secrets "redisUsername" }} - name: REDIS_USERNAME valueFrom: secretKeyRef: name: gundeck key: redisUsername {{- end }} - {{- if hasKey .Values.secrets "redisPassword" }} + {{- if hasKey .Values.gundeck.secrets "redisPassword" }} - name: REDIS_PASSWORD valueFrom: secretKeyRef: name: gundeck key: redisPassword {{- end }} - {{- if hasKey .Values.secrets "redisAdditionalWriteUsername" }} + {{- if hasKey .Values.gundeck.secrets "redisAdditionalWriteUsername" }} - name: REDIS_ADDITIONAL_WRITE_USERNAME valueFrom: secretKeyRef: name: gundeck key: redisAdditionalWriteUsername {{- end }} - {{- if hasKey .Values.secrets "redisAdditionalWritePassword" }} + {{- if hasKey .Values.gundeck.secrets "redisAdditionalWritePassword" }} - name: REDIS_ADDITIONAL_WRITE_PASSWORD valueFrom: secretKeyRef: @@ -139,8 +139,8 @@ spec: key: redisAdditionalWritePassword {{- end }} - name: AWS_REGION - value: "{{ .Values.config.aws.region }}" - {{- with .Values.config.proxy }} + value: "{{ .Values.gundeck.config.aws.region }}" + {{- with .Values.gundeck.config.proxy }} {{- if .httpProxy }} - name: http_proxy value: {{ .httpProxy | quote }} @@ -161,20 +161,20 @@ spec: {{- end }} {{- end }} ports: - - containerPort: {{ .Values.service.internalPort }} + - containerPort: {{ .Values.gundeck.service.internalPort }} livenessProbe: httpGet: scheme: HTTP path: /i/status - port: {{ .Values.service.internalPort }} + port: {{ .Values.gundeck.service.internalPort }} readinessProbe: httpGet: scheme: HTTP path: /i/status - port: {{ .Values.service.internalPort }} + port: {{ .Values.gundeck.service.internalPort }} lifecycle: preStop: exec: command: ["sh", "-c", "sleep 10"] resources: -{{ toYaml .Values.resources | indent 12 }} +{{ toYaml .Values.gundeck.resources | indent 12 }} diff --git a/charts/gundeck/templates/poddisruptionbudget.yaml b/charts/wire-server/templates/gundeck/poddisruptionbudget.yaml similarity index 72% rename from charts/gundeck/templates/poddisruptionbudget.yaml rename to charts/wire-server/templates/gundeck/poddisruptionbudget.yaml index adf5531eac4..7579db09fd2 100644 --- a/charts/gundeck/templates/poddisruptionbudget.yaml +++ b/charts/wire-server/templates/gundeck/poddisruptionbudget.yaml @@ -1,4 +1,4 @@ -{{- if gt (int .Values.replicaCount) 1 }} +{{- if gt (int .Values.gundeck.replicaCount) 1 }} apiVersion: policy/v1 kind: PodDisruptionBudget metadata: @@ -9,7 +9,7 @@ metadata: release: {{ .Release.Name }} heritage: {{ .Release.Service }} spec: - maxUnavailable: {{ sub (int .Values.replicaCount) 1 }} + maxUnavailable: {{ sub (int .Values.gundeck.replicaCount) 1 }} selector: matchLabels: app: gundeck diff --git a/charts/gundeck/templates/redis-ca-secret.yaml b/charts/wire-server/templates/gundeck/redis-ca-secret.yaml similarity index 66% rename from charts/gundeck/templates/redis-ca-secret.yaml rename to charts/wire-server/templates/gundeck/redis-ca-secret.yaml index de1f752e55a..a82eab555cb 100644 --- a/charts/gundeck/templates/redis-ca-secret.yaml +++ b/charts/wire-server/templates/gundeck/redis-ca-secret.yaml @@ -1,5 +1,5 @@ --- -{{- if not (empty .Values.config.redis.tlsCa) }} +{{- if not (empty .Values.gundeck.config.redis.tlsCa) }} apiVersion: v1 kind: Secret metadata: @@ -11,10 +11,10 @@ metadata: heritage: "{{ .Release.Service }}" type: Opaque data: - ca.pem: {{ .Values.config.redis.tlsCa | b64enc | quote }} + ca.pem: {{ .Values.gundeck.config.redis.tlsCa | b64enc | quote }} {{- end }} --- -{{- if not (empty .Values.config.redis.additionalTlsCa) }} +{{- if not (empty .Values.gundeck.config.redis.additionalTlsCa) }} apiVersion: v1 kind: Secret metadata: @@ -26,5 +26,5 @@ metadata: heritage: "{{ .Release.Service }}" type: Opaque data: - ca.pem: {{ .Values.config.redis.additionalTlsCa | b64enc | quote }} + ca.pem: {{ .Values.gundeck.config.redis.additionalTlsCa | b64enc | quote }} {{- end }} diff --git a/charts/gundeck/templates/secret.yaml b/charts/wire-server/templates/gundeck/secret.yaml similarity index 92% rename from charts/gundeck/templates/secret.yaml rename to charts/wire-server/templates/gundeck/secret.yaml index 67c61afc220..b1f744ff600 100644 --- a/charts/gundeck/templates/secret.yaml +++ b/charts/wire-server/templates/gundeck/secret.yaml @@ -1,4 +1,4 @@ -{{- if not (empty .Values.secrets) }} +{{- if not (empty .Values.gundeck.secrets) }} apiVersion: v1 kind: Secret metadata: @@ -10,7 +10,7 @@ metadata: heritage: "{{ .Release.Service }}" type: Opaque data: - {{- with .Values.secrets }} + {{- with .Values.gundeck.secrets }} rabbitmqUsername: {{ .rabbitmq.username | b64enc | quote }} rabbitmqPassword: {{ .rabbitmq.password | b64enc | quote }} {{- if hasKey . "awsKeyId" }} diff --git a/charts/gundeck/templates/service.yaml b/charts/wire-server/templates/gundeck/service.yaml similarity index 82% rename from charts/gundeck/templates/service.yaml rename to charts/wire-server/templates/gundeck/service.yaml index 1227bec6064..ce905982cab 100644 --- a/charts/gundeck/templates/service.yaml +++ b/charts/wire-server/templates/gundeck/service.yaml @@ -17,8 +17,8 @@ spec: type: ClusterIP ports: - name: http - port: {{ .Values.service.externalPort }} - targetPort: {{ .Values.service.internalPort }} + port: {{ .Values.gundeck.service.externalPort }} + targetPort: {{ .Values.gundeck.service.internalPort }} selector: app: gundeck release: {{ .Release.Name }} diff --git a/charts/gundeck/templates/serviceaccount.yaml b/charts/wire-server/templates/gundeck/serviceaccount.yaml similarity index 52% rename from charts/gundeck/templates/serviceaccount.yaml rename to charts/wire-server/templates/gundeck/serviceaccount.yaml index 59bdd51128e..3d30d57adb5 100644 --- a/charts/gundeck/templates/serviceaccount.yaml +++ b/charts/wire-server/templates/gundeck/serviceaccount.yaml @@ -1,16 +1,16 @@ -{{- if .Values.serviceAccount.create -}} +{{- if .Values.gundeck.serviceAccount.create -}} apiVersion: v1 kind: ServiceAccount metadata: - name: {{ .Values.serviceAccount.name }} + name: {{ .Values.gundeck.serviceAccount.name }} labels: app: gundeck chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} - {{- with .Values.serviceAccount.annotations }} + {{- with .Values.gundeck.serviceAccount.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} -automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} +automountServiceAccountToken: {{ .Values.gundeck.serviceAccount.automountServiceAccountToken }} {{- end }} diff --git a/charts/gundeck/templates/servicemonitor.yaml b/charts/wire-server/templates/gundeck/servicemonitor.yaml similarity index 87% rename from charts/gundeck/templates/servicemonitor.yaml rename to charts/wire-server/templates/gundeck/servicemonitor.yaml index bd1adc4c1d6..2b7b22100d0 100644 --- a/charts/gundeck/templates/servicemonitor.yaml +++ b/charts/wire-server/templates/gundeck/servicemonitor.yaml @@ -1,4 +1,4 @@ -{{- if .Values.metrics.serviceMonitor.enabled }} +{{- if .Values.gundeck.metrics.serviceMonitor.enabled }} apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: diff --git a/charts/gundeck/templates/tests/configmap.yaml b/charts/wire-server/templates/gundeck/tests/configmap.yaml similarity index 96% rename from charts/gundeck/templates/tests/configmap.yaml rename to charts/wire-server/templates/gundeck/tests/configmap.yaml index 3ceb39cec92..acba48d7f16 100644 --- a/charts/gundeck/templates/tests/configmap.yaml +++ b/charts/wire-server/templates/gundeck/tests/configmap.yaml @@ -9,7 +9,7 @@ data: integration.yaml: | gundeck: host: gundeck - port: {{ .Values.service.internalPort }} + port: {{ .Values.gundeck.service.internalPort }} cannon: host: cannon diff --git a/charts/gundeck/templates/tests/gundeck-integration.yaml b/charts/wire-server/templates/gundeck/tests/gundeck-integration.yaml similarity index 70% rename from charts/gundeck/templates/tests/gundeck-integration.yaml rename to charts/wire-server/templates/gundeck/tests/gundeck-integration.yaml index a2aa75e52cd..b70752b3ead 100644 --- a/charts/gundeck/templates/tests/gundeck-integration.yaml +++ b/charts/wire-server/templates/gundeck/tests/gundeck-integration.yaml @@ -13,20 +13,20 @@ spec: - name: "gundeck-config" configMap: name: "gundeck" - {{- if eq (include "useCassandraTLS" .Values.config) "true" }} + {{- if eq (include "useCassandraTLS" .Values.gundeck.config.cassandra) "true" }} - name: "gundeck-cassandra" secret: - secretName: {{ (include "tlsSecretRef" .Values.config | fromYaml).name }} + secretName: {{ (include "gundeck.tlsSecretRef" .Values.gundeck.config | fromYaml).name }} {{- end}} - {{- if eq (include "configureRedisCa" .Values.config) "true" }} + {{- if eq (include "gundeck.configureRedisCa" .Values.gundeck.config) "true" }} - name: "redis-ca" secret: - secretName: {{ include "redisTlsSecretName" .Values.config }} + secretName: {{ include "gundeck.redisTlsSecretName" .Values.gundeck.config }} {{- end }} - {{- if .Values.config.rabbitmq.tlsCaSecretRef }} + {{- if .Values.gundeck.config.rabbitmq.tlsCaSecretRef }} - name: "rabbitmq-ca" secret: - secretName: {{ .Values.config.rabbitmq.tlsCaSecretRef.name }} + secretName: {{ .Values.gundeck.config.rabbitmq.tlsCaSecretRef.name }} {{- end }} containers: - name: integration @@ -44,7 +44,7 @@ spec: exit_code=$? fi - {{- if .Values.tests.config.uploadXml }} + {{- if .Values.gundeck.tests.config.uploadXml }} # In case a different S3 compliant storage is used to upload test result. if ! [[ -z "${UPLOAD_XML_AWS_ACCESS_KEY_ID+x}" ]]; then export AWS_ACCESS_KEY_ID="$UPLOAD_XML_AWS_ACCESS_KEY_ID" @@ -59,25 +59,25 @@ spec: {{- end }} exit $exit_code - image: "{{ .Values.image.repository }}-integration:{{ .Values.image.tag }}" + image: "{{ .Values.gundeck.image.repository }}-integration:{{ .Values.gundeck.image.tag }}" {{- if eq (include "includeSecurityContext" .) "true" }} securityContext: - {{- toYaml .Values.podSecurityContext | nindent 6 }} + {{- toYaml .Values.gundeck.podSecurityContext | nindent 6 }} {{- end }} volumeMounts: - name: "gundeck-integration" mountPath: "/etc/wire/integration" - name: "gundeck-config" mountPath: "/etc/wire/gundeck/conf" - {{- if eq (include "useCassandraTLS" .Values.config) "true" }} + {{- if eq (include "useCassandraTLS" .Values.gundeck.config.cassandra) "true" }} - name: "gundeck-cassandra" mountPath: "/etc/wire/gundeck/cassandra" {{- end }} - {{- if eq (include "configureRedisCa" .Values.config) "true" }} + {{- if eq (include "gundeck.configureRedisCa" .Values.gundeck.config) "true" }} - name: "redis-ca" mountPath: "/etc/wire/gundeck/redis-ca/" {{- end }} - {{- if .Values.config.rabbitmq.tlsCaSecretRef }} + {{- if .Values.gundeck.config.rabbitmq.tlsCaSecretRef }} - name: "rabbitmq-ca" mountPath: "/etc/wire/gundeck/rabbitmq-ca/" {{- end }} @@ -96,38 +96,38 @@ spec: value: "guest" - name: RABBITMQ_PASSWORD value: "guest" - {{- if hasKey .Values.secrets "redisUsername" }} + {{- if hasKey .Values.gundeck.secrets "redisUsername" }} - name: REDIS_USERNAME valueFrom: secretKeyRef: name: gundeck key: redisUsername {{- end }} - {{- if hasKey .Values.secrets "redisPassword" }} + {{- if hasKey .Values.gundeck.secrets "redisPassword" }} - name: REDIS_PASSWORD valueFrom: secretKeyRef: name: gundeck key: redisPassword {{- end }} - {{- if and (hasKey .Values.tests "secrets") (hasKey .Values.tests.secrets "redisAdditionalWriteUsername") }} + {{- if and (hasKey .Values.gundeck.tests "secrets") (hasKey .Values.gundeck.tests.secrets "redisAdditionalWriteUsername") }} - name: REDIS_ADDITIONAL_WRITE_USERNAME valueFrom: secretKeyRef: name: gundeck-integration key: redisAdditionalWriteUsername {{- end }} - {{- if and (hasKey .Values.tests "secrets") (hasKey .Values.tests.secrets "redisAdditionalWritePassword") }} + {{- if and (hasKey .Values.gundeck.tests "secrets") (hasKey .Values.gundeck.tests.secrets "redisAdditionalWritePassword") }} - name: REDIS_ADDITIONAL_WRITE_PASSWORD valueFrom: secretKeyRef: name: gundeck-integration key: redisAdditionalWritePassword {{- end }} - {{- if .Values.tests.config.uploadXml }} + {{- if .Values.gundeck.tests.config.uploadXml }} - name: UPLOAD_XML_S3_BASE_URL - value: {{ .Values.tests.config.uploadXml.baseUrl }} - {{- if .Values.tests.secrets.uploadXmlAwsAccessKeyId }} + value: {{ .Values.gundeck.tests.config.uploadXml.baseUrl }} + {{- if .Values.gundeck.tests.secrets.uploadXmlAwsAccessKeyId }} - name: UPLOAD_XML_AWS_ACCESS_KEY_ID valueFrom: secretKeyRef: diff --git a/charts/gundeck/templates/tests/secret.yaml b/charts/wire-server/templates/gundeck/tests/secret.yaml similarity index 90% rename from charts/gundeck/templates/tests/secret.yaml rename to charts/wire-server/templates/gundeck/tests/secret.yaml index ff5712545c8..60aed14a3a4 100644 --- a/charts/gundeck/templates/tests/secret.yaml +++ b/charts/wire-server/templates/gundeck/tests/secret.yaml @@ -1,4 +1,4 @@ -{{- if not (empty .Values.tests.secrets) }} +{{- if not (empty .Values.gundeck.tests.secrets) }} apiVersion: v1 kind: Secret metadata: @@ -10,7 +10,7 @@ metadata: heritage: "{{ .Release.Service }}" type: Opaque data: - {{- with .Values.tests.secrets }} + {{- with .Values.gundeck.tests.secrets }} {{- if hasKey . "uploadXmlAwsAccessKeyId" }} uploadXmlAwsAccessKeyId: {{ .uploadXmlAwsAccessKeyId | b64enc | quote }} {{- end }} diff --git a/charts/wire-server/templates/proxy/configmap.yaml b/charts/wire-server/templates/proxy/configmap.yaml new file mode 100644 index 00000000000..4ee4424b35e --- /dev/null +++ b/charts/wire-server/templates/proxy/configmap.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: "proxy" +data: + proxy.yaml: | + logFormat: {{ .Values.proxy.config.logFormat }} + logLevel: {{ .Values.proxy.config.logLevel }} + logNetStrings: {{ .Values.proxy.config.logNetStrings }} + disabledAPIVersions: {{ toJson .Values.proxy.config.disabledAPIVersions }} + proxy: + host: 0.0.0.0 + port: {{ .Values.proxy.service.internalPort }} + httpPoolSize: 1000 + maxConns: 5000 + secretsConfig: /etc/wire/proxy/secrets/proxy.config diff --git a/charts/proxy/templates/deployment.yaml b/charts/wire-server/templates/proxy/deployment.yaml similarity index 77% rename from charts/proxy/templates/deployment.yaml rename to charts/wire-server/templates/proxy/deployment.yaml index 02676553a1b..430cd356af7 100644 --- a/charts/proxy/templates/deployment.yaml +++ b/charts/wire-server/templates/proxy/deployment.yaml @@ -8,12 +8,12 @@ metadata: release: {{ .Release.Name }} heritage: {{ .Release.Service }} spec: - replicas: {{ .Values.replicaCount }} + replicas: {{ .Values.proxy.replicaCount }} strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 0 - maxSurge: {{ .Values.replicaCount }} + maxSurge: {{ .Values.proxy.replicaCount }} selector: matchLabels: app: proxy @@ -24,8 +24,8 @@ spec: release: {{ .Release.Name }} annotations: # An annotation of the configmap checksum ensures changes to the configmap cause a redeployment upon `helm upgrade` - checksum/configmap: {{ include (print .Template.BasePath "/configmap.yaml") . | sha256sum }} - checksum/secret: {{ include (print .Template.BasePath "/secret.yaml") . | sha256sum }} + checksum/configmap: {{ include (print .Template.BasePath "/proxy/configmap.yaml") . | sha256sum }} + checksum/secret: {{ include (print .Template.BasePath "/proxy/secret.yaml") . | sha256sum }} spec: topologySpreadConstraints: - maxSkew: 1 @@ -43,11 +43,11 @@ spec: secretName: "proxy" containers: - name: proxy - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" - imagePullPolicy: {{ default "" .Values.imagePullPolicy | quote }} + image: "{{ .Values.proxy.image.repository }}:{{ .Values.proxy.image.tag }}" + imagePullPolicy: {{ default "" .Values.proxy.imagePullPolicy | quote }} {{- if eq (include "includeSecurityContext" .) "true" }} securityContext: - {{- toYaml .Values.podSecurityContext | nindent 12 }} + {{- toYaml .Values.proxy.podSecurityContext | nindent 12 }} {{- end }} volumeMounts: - name: "proxy-secrets" @@ -55,7 +55,7 @@ spec: - name: "proxy-config" mountPath: "/etc/wire/proxy/conf" env: - {{- with .Values.config.proxy }} + {{- with .Values.proxy.config.proxy }} {{- if .httpProxy }} - name: http_proxy value: {{ .httpProxy | quote }} @@ -76,16 +76,16 @@ spec: {{- end }} {{- end }} ports: - - containerPort: {{ .Values.service.internalPort }} + - containerPort: {{ .Values.proxy.service.internalPort }} livenessProbe: httpGet: scheme: HTTP path: /i/status - port: {{ .Values.service.internalPort }} + port: {{ .Values.proxy.service.internalPort }} readinessProbe: httpGet: scheme: HTTP path: /i/status - port: {{ .Values.service.internalPort }} + port: {{ .Values.proxy.service.internalPort }} resources: -{{ toYaml .Values.resources | indent 12 }} +{{ toYaml .Values.proxy.resources | indent 12 }} diff --git a/charts/proxy/templates/poddisruptionbudget.yaml b/charts/wire-server/templates/proxy/poddisruptionbudget.yaml similarity index 72% rename from charts/proxy/templates/poddisruptionbudget.yaml rename to charts/wire-server/templates/proxy/poddisruptionbudget.yaml index c828d0368dc..614bb54b077 100644 --- a/charts/proxy/templates/poddisruptionbudget.yaml +++ b/charts/wire-server/templates/proxy/poddisruptionbudget.yaml @@ -1,4 +1,4 @@ -{{- if gt (int .Values.replicaCount) 1 }} +{{- if gt (int .Values.proxy.replicaCount) 1 }} apiVersion: policy/v1 kind: PodDisruptionBudget metadata: @@ -9,7 +9,7 @@ metadata: release: {{ .Release.Name }} heritage: {{ .Release.Service }} spec: - maxUnavailable: {{ sub (int .Values.replicaCount) 1 }} + maxUnavailable: {{ sub (int .Values.proxy.replicaCount) 1 }} selector: matchLabels: app: proxy diff --git a/charts/proxy/templates/secret.yaml b/charts/wire-server/templates/proxy/secret.yaml similarity index 75% rename from charts/proxy/templates/secret.yaml rename to charts/wire-server/templates/proxy/secret.yaml index de452b7fca7..defd1a01640 100644 --- a/charts/proxy/templates/secret.yaml +++ b/charts/wire-server/templates/proxy/secret.yaml @@ -9,4 +9,4 @@ metadata: heritage: "{{ .Release.Service }}" type: Opaque data: - proxy.config: {{ .Values.secrets.proxy_config | b64enc | quote }} + proxy.config: {{ .Values.proxy.secrets.proxy_config | b64enc | quote }} diff --git a/charts/proxy/templates/service.yaml b/charts/wire-server/templates/proxy/service.yaml similarity index 82% rename from charts/proxy/templates/service.yaml rename to charts/wire-server/templates/proxy/service.yaml index 478ad3d6a37..4a4760460cd 100644 --- a/charts/proxy/templates/service.yaml +++ b/charts/wire-server/templates/proxy/service.yaml @@ -17,8 +17,8 @@ spec: type: ClusterIP ports: - name: http - port: {{ .Values.service.externalPort }} - targetPort: {{ .Values.service.internalPort }} + port: {{ .Values.proxy.service.externalPort }} + targetPort: {{ .Values.proxy.service.internalPort }} selector: app: proxy release: {{ .Release.Name }} diff --git a/charts/proxy/templates/servicemonitor.yaml b/charts/wire-server/templates/proxy/servicemonitor.yaml similarity index 87% rename from charts/proxy/templates/servicemonitor.yaml rename to charts/wire-server/templates/proxy/servicemonitor.yaml index 88120fe7cdb..eedad5d1a4b 100644 --- a/charts/proxy/templates/servicemonitor.yaml +++ b/charts/wire-server/templates/proxy/servicemonitor.yaml @@ -1,4 +1,4 @@ -{{- if .Values.metrics.serviceMonitor.enabled }} +{{- if .Values.proxy.metrics.serviceMonitor.enabled }} apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: diff --git a/charts/spar/templates/cassandra-secret.yaml b/charts/wire-server/templates/spar/cassandra-secret.yaml similarity index 70% rename from charts/spar/templates/cassandra-secret.yaml rename to charts/wire-server/templates/spar/cassandra-secret.yaml index 0a480e01bb0..c0d419e8654 100644 --- a/charts/spar/templates/cassandra-secret.yaml +++ b/charts/wire-server/templates/spar/cassandra-secret.yaml @@ -1,5 +1,5 @@ {{/* Secret for the provided Cassandra TLS CA. */}} -{{- if not (empty .Values.config.cassandra.tlsCa) }} +{{- if not (empty .Values.spar.config.cassandra.tlsCa) }} apiVersion: v1 kind: Secret metadata: @@ -11,5 +11,5 @@ metadata: heritage: "{{ .Release.Service }}" type: Opaque data: - ca.pem: {{ .Values.config.cassandra.tlsCa | b64enc | quote }} + ca.pem: {{ .Values.spar.config.cassandra.tlsCa | b64enc | quote }} {{- end }} diff --git a/charts/spar/templates/configmap.yaml b/charts/wire-server/templates/spar/configmap.yaml similarity index 85% rename from charts/spar/templates/configmap.yaml rename to charts/wire-server/templates/spar/configmap.yaml index 2b7af0e3872..7976fb14f40 100644 --- a/charts/spar/templates/configmap.yaml +++ b/charts/wire-server/templates/spar/configmap.yaml @@ -3,7 +3,7 @@ kind: ConfigMap metadata: name: "spar" data: -{{- with .Values.config }} +{{- with .Values.spar.config }} spar.yaml: | logFormat: {{ .logFormat }} logLevel: {{ .logLevel }} @@ -25,8 +25,8 @@ data: {{- if hasKey .cassandra "filterNodesByDatacentre" }} filterNodesByDatacentre: {{ .cassandra.filterNodesByDatacentre }} {{- end }} - {{- if eq (include "useCassandraTLS" .) "true" }} - tlsCa: /etc/wire/spar/cassandra/{{- (include "tlsSecretRef" . | fromYaml).key }} + {{- if eq (include "useCassandraTLS" .cassandra) "true" }} + tlsCa: /etc/wire/spar/cassandra/{{- (include "spar.tlsSecretRef" . | fromYaml).key }} {{- end }} maxttlAuthreq: {{ .maxttlAuthreq }} @@ -45,7 +45,7 @@ data: logLevel: {{ .logLevel }} spHost: 0.0.0.0 - spPort: {{ $.Values.service.externalPort }} + spPort: {{ $.Values.spar.service.externalPort }} {{- if .domainConfigs }} spDomainConfigs: {{- range $key, $value := .domainConfigs }} @@ -62,5 +62,5 @@ data: {{- required "No multi-ingress: config.contacts required" .contacts | toYaml | nindent 8 }} {{- end }} - scimBaseUri: {{ include "computeScimBaseUri" . | quote }} + scimBaseUri: {{ include "spar.computeScimBaseUri" . | quote }} {{- end }} diff --git a/charts/spar/templates/deployment.yaml b/charts/wire-server/templates/spar/deployment.yaml similarity index 71% rename from charts/spar/templates/deployment.yaml rename to charts/wire-server/templates/spar/deployment.yaml index 5176bf3ebb2..f1f7b5b910d 100644 --- a/charts/spar/templates/deployment.yaml +++ b/charts/wire-server/templates/spar/deployment.yaml @@ -8,12 +8,12 @@ metadata: release: {{ .Release.Name }} heritage: {{ .Release.Service }} spec: - replicas: {{ .Values.replicaCount }} + replicas: {{ .Values.spar.replicaCount }} strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 0 - maxSurge: {{ .Values.replicaCount }} + maxSurge: {{ .Values.spar.replicaCount }} selector: matchLabels: app: spar @@ -24,7 +24,7 @@ spec: release: {{ .Release.Name }} annotations: # An annotation of the configmap checksum ensures changes to the configmap cause a redeployment upon `helm upgrade` - checksum/configmap: {{ include (print .Template.BasePath "/configmap.yaml") . | sha256sum }} + checksum/configmap: {{ include (print .Template.BasePath "/spar/configmap.yaml") . | sha256sum }} spec: topologySpreadConstraints: - maxSkew: 1 @@ -37,28 +37,28 @@ spec: - name: "spar-config" configMap: name: "spar" - {{- if eq (include "useCassandraTLS" .Values.config) "true" }} + {{- if eq (include "useCassandraTLS" .Values.spar.config.cassandra) "true" }} - name: "spar-cassandra" secret: - secretName: {{ (include "tlsSecretRef" .Values.config | fromYaml).name }} + secretName: {{ (include "spar.tlsSecretRef" .Values.spar.config | fromYaml).name }} {{- end}} containers: - name: spar - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" - imagePullPolicy: {{ default "" .Values.imagePullPolicy | quote }} + image: "{{ .Values.spar.image.repository }}:{{ .Values.spar.image.tag }}" + imagePullPolicy: {{ default "" .Values.spar.imagePullPolicy | quote }} {{- if eq (include "includeSecurityContext" .) "true" }} securityContext: - {{- toYaml .Values.podSecurityContext | nindent 12 }} + {{- toYaml .Values.spar.podSecurityContext | nindent 12 }} {{- end }} volumeMounts: - name: "spar-config" mountPath: "/etc/wire/spar/conf" - {{- if eq (include "useCassandraTLS" .Values.config) "true" }} + {{- if eq (include "useCassandraTLS" .Values.spar.config.cassandra) "true" }} - name: "spar-cassandra" mountPath: "/etc/wire/spar/cassandra" {{- end }} env: - {{- with .Values.config.proxy }} + {{- with .Values.spar.config.proxy }} {{- if .httpProxy }} - name: http_proxy value: {{ .httpProxy | quote }} @@ -79,16 +79,16 @@ spec: {{- end }} {{- end }} ports: - - containerPort: {{ .Values.service.internalPort }} + - containerPort: {{ .Values.spar.service.internalPort }} livenessProbe: httpGet: scheme: HTTP path: /i/status - port: {{ .Values.service.internalPort }} + port: {{ .Values.spar.service.internalPort }} readinessProbe: httpGet: scheme: HTTP path: /i/status - port: {{ .Values.service.internalPort }} + port: {{ .Values.spar.service.internalPort }} resources: -{{ toYaml .Values.resources | indent 12 }} +{{ toYaml .Values.spar.resources | indent 12 }} diff --git a/charts/spar/templates/poddisruptionbudget.yaml b/charts/wire-server/templates/spar/poddisruptionbudget.yaml similarity index 73% rename from charts/spar/templates/poddisruptionbudget.yaml rename to charts/wire-server/templates/spar/poddisruptionbudget.yaml index d7eb680a3ba..39afc5b51be 100644 --- a/charts/spar/templates/poddisruptionbudget.yaml +++ b/charts/wire-server/templates/spar/poddisruptionbudget.yaml @@ -1,4 +1,4 @@ -{{- if gt (int .Values.replicaCount) 1 }} +{{- if gt (int .Values.spar.replicaCount) 1 }} apiVersion: policy/v1 kind: PodDisruptionBudget metadata: @@ -9,7 +9,7 @@ metadata: release: {{ .Release.Name }} heritage: {{ .Release.Service }} spec: - maxUnavailable: {{ sub (int .Values.replicaCount) 1 }} + maxUnavailable: {{ sub (int .Values.spar.replicaCount) 1 }} selector: matchLabels: app: spar diff --git a/charts/spar/templates/service.yaml b/charts/wire-server/templates/spar/service.yaml similarity index 82% rename from charts/spar/templates/service.yaml rename to charts/wire-server/templates/spar/service.yaml index 46e652c65be..413c879bbe4 100644 --- a/charts/spar/templates/service.yaml +++ b/charts/wire-server/templates/spar/service.yaml @@ -17,8 +17,8 @@ spec: type: ClusterIP ports: - name: http - port: {{ .Values.service.externalPort }} - targetPort: {{ .Values.service.internalPort }} + port: {{ .Values.spar.service.externalPort }} + targetPort: {{ .Values.spar.service.internalPort }} selector: app: spar release: {{ .Release.Name }} diff --git a/charts/spar/templates/servicemonitor.yaml b/charts/wire-server/templates/spar/servicemonitor.yaml similarity index 87% rename from charts/spar/templates/servicemonitor.yaml rename to charts/wire-server/templates/spar/servicemonitor.yaml index f2b23703b61..7df10b48409 100644 --- a/charts/spar/templates/servicemonitor.yaml +++ b/charts/wire-server/templates/spar/servicemonitor.yaml @@ -1,4 +1,4 @@ -{{- if .Values.metrics.serviceMonitor.enabled }} +{{- if .Values.spar.metrics.serviceMonitor.enabled }} apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: diff --git a/charts/spar/templates/tests/configmap.yaml b/charts/wire-server/templates/spar/tests/configmap.yaml similarity index 100% rename from charts/spar/templates/tests/configmap.yaml rename to charts/wire-server/templates/spar/tests/configmap.yaml diff --git a/charts/spar/templates/tests/secret.yaml b/charts/wire-server/templates/spar/tests/secret.yaml similarity index 91% rename from charts/spar/templates/tests/secret.yaml rename to charts/wire-server/templates/spar/tests/secret.yaml index 1be597127b8..fbaa41c6e47 100644 --- a/charts/spar/templates/tests/secret.yaml +++ b/charts/wire-server/templates/spar/tests/secret.yaml @@ -9,7 +9,7 @@ metadata: heritage: "{{ .Release.Service }}" type: Opaque data: - {{- with .Values.tests.secrets }} + {{- with .Values.spar.tests.secrets }} uploadXmlAwsAccessKeyId: {{ .uploadXmlAwsAccessKeyId | b64enc | quote }} uploadXmlAwsSecretAccessKey: {{ .uploadXmlAwsSecretAccessKey | b64enc | quote }} {{- end }} diff --git a/charts/spar/templates/tests/spar-integration.yaml b/charts/wire-server/templates/spar/tests/spar-integration.yaml similarity index 78% rename from charts/spar/templates/tests/spar-integration.yaml rename to charts/wire-server/templates/spar/tests/spar-integration.yaml index 9cae732bfb3..259018d6338 100644 --- a/charts/spar/templates/tests/spar-integration.yaml +++ b/charts/wire-server/templates/spar/tests/spar-integration.yaml @@ -16,17 +16,17 @@ spec: - name: "spar-config" configMap: name: "spar" - {{- if eq (include "useCassandraTLS" .Values.config) "true" }} + {{- if eq (include "useCassandraTLS" .Values.spar.config.cassandra) "true" }} - name: "spar-cassandra" secret: - secretName: {{ (include "tlsSecretRef" .Values.config | fromYaml).name }} + secretName: {{ (include "spar.tlsSecretRef" .Values.spar.config | fromYaml).name }} {{- end}} containers: - name: integration - image: "{{ .Values.image.repository }}-integration:{{ .Values.image.tag }}" + image: "{{ .Values.spar.image.repository }}-integration:{{ .Values.spar.image.tag }}" {{- if eq (include "includeSecurityContext" .) "true" }} securityContext: - {{- toYaml .Values.podSecurityContext | nindent 6 }} + {{- toYaml .Values.spar.podSecurityContext | nindent 6 }} {{- end }} command: - /bin/bash @@ -40,7 +40,7 @@ spec: exit_code=$? fi - {{- if .Values.tests.config.uploadXml }} + {{- if .Values.spar.tests.config.uploadXml }} # In case a different S3 compliant storage is used to upload test result. if ! [[ -z "${UPLOAD_XML_AWS_ACCESS_KEY_ID+x}" ]]; then export AWS_ACCESS_KEY_ID="$UPLOAD_XML_AWS_ACCESS_KEY_ID" @@ -61,7 +61,7 @@ spec: mountPath: "/etc/wire/integration" - name: "spar-config" mountPath: "/etc/wire/spar/conf" - {{- if eq (include "useCassandraTLS" .Values.config) "true" }} + {{- if eq (include "useCassandraTLS" .Values.spar.config.cassandra) "true" }} - name: "spar-cassandra" mountPath: "/etc/wire/spar/cassandra" {{- end }} @@ -74,10 +74,10 @@ spec: value: /tmp/ - name: JUNIT_SUITE_NAME value: spar - {{- if .Values.tests.config.uploadXml }} + {{- if .Values.spar.tests.config.uploadXml }} - name: UPLOAD_XML_S3_BASE_URL - value: {{ .Values.tests.config.uploadXml.baseUrl }} - {{- if .Values.tests.secrets.uploadXmlAwsAccessKeyId }} + value: {{ .Values.spar.tests.config.uploadXml.baseUrl }} + {{- if .Values.spar.tests.secrets.uploadXmlAwsAccessKeyId }} - name: UPLOAD_XML_AWS_ACCESS_KEY_ID valueFrom: secretKeyRef: diff --git a/charts/wire-server/values.yaml b/charts/wire-server/values.yaml index 55900d494fc..5b6f0c99b0e 100644 --- a/charts/wire-server/values.yaml +++ b/charts/wire-server/values.yaml @@ -7,7 +7,1363 @@ tags: legalhold: false - federation: false # see also {background-worker, brig, cargohold, galley}.config.enableFederation + federation: false backoffice: false mlsstats: false integration: false + +galley: + replicaCount: 3 + image: + repository: quay.io/wire/galley + tag: do-not-use + schemaRepository: quay.io/wire/galley-schema + service: + externalPort: 8080 + internalPort: 8080 + metrics: + serviceMonitor: + enabled: false + resources: + requests: + memory: "100Mi" + cpu: "100m" + limits: + memory: "500Mi" + # This is not supported for production use, only here for testing: + # preStop: + # exec: + # command: ["sh", "-c", "curl http://acme.example"] + config: + logLevel: Info + logFormat: StructuredJSON + logNetStrings: false + enableFederation: false # keep in sync with background-worker, brig and cargohold charts' config.enableFederation as well as wire-server chart's tags.federation + cassandra: + host: aws-cassandra + replicaCount: 3 + # To enable TLS provide a CA: + # tlsCa: + # + # Or refer to an existing secret (containing the CA): + # tlsCaSecretRef: + # name: + # key: + + # Postgres connection settings + # + # Values are described in https://www.postgresql.org/docs/17/libpq-connect.html#LIBPQ-PARAMKEYWORDS + # To set the password via a brig secret see `secrets.pgPassword`. + # + # `additionalVolumeMounts` and `additionalVolumes` can be used to mount + # additional files (e.g. certificates) into the galley container. This way + # does not work for password files (parameter `passfile`), because + # libpq-connect requires access rights (mask 0600) for them that we cannot + # provide for random uids. + # + # Below is an example configuration we're using for our CI tests. + postgresql: + host: postgresql # DNS name without protocol + port: "5432" + user: wire-server + dbname: wire-server + postgresqlPool: + size: 100 + acquisitionTimeout: 10s + agingTimeout: 1d + idlenessTimeout: 10m + + # Not used if enableFederation is false + rabbitmq: + host: rabbitmq + port: 5672 + vHost: / + enableTls: false + insecureSkipVerifyTls: false + # tlsCaSecretRef: + # name: + # key: + + postgresMigration: + conversation: cassandra + conversationCodes: cassandra + teamFeatures: cassandra + settings: + httpPoolSize: 128 + maxTeamSize: 10000 + exposeInvitationURLsTeamAllowlist: [] + maxConvSize: 500 + intraListing: true + # Either `conversationCodeURI` or `multiIngress` must be set + # + # `conversationCodeURI` is the URI prefix for conversation invitation links + # It should be of form https://{ACCOUNT_PAGES}/conversation-join/ + conversationCodeURI: null + # + # `multiIngress` is a `Z-Host` depended setting of conversationCodeURI. + # Use this only if you want to expose the instance on multiple ingresses. + # If set it must a map from `Z-Host` to URI prefix + # Example: + # multiIngress: + # wire.example: https://accounts.wire.example/conversation-join/ + # example.net: https://accounts.example.net/conversation-join/ + multiIngress: null + # Disable one ore more API versions. Please make sure the configuration value is the same in all these charts: + # brig, cannon, cargohold, galley, gundeck, proxy, spar. + disabledAPIVersions: [development] + # The lifetime of a conversation guest link in seconds. Must be a value 0 < x <= 31536000 (365 days) + # Default is 31536000 (365 days) if not set + guestLinkTTLSeconds: 31536000 + passwordHashingOptions: + algorithm: scrypt # or argon2id + # When algorithm is argon2id, these can be configured: + # iterations: + # parallelism: + # memory: + passwordHashingRateLimit: + ipAddrLimit: + burst: 5 + inverseRate: 300000000 # 5 mins, makes it 12 reqs/hour + userLimit: + burst: 5 + inverseRate: 60000000 # 1 min, makes it 60 req/hour + internalLimit: + burst: 10 + inverseRate: 0 # No rate limiting for internal use + ipv4CidrBlock: 32 # Only block individual IP addresses + ipv6CidrBlock: 64 # Block /64 range at a time. + ipAddressExceptions: [] + maxRateLimitedKeys: 100000 # Estimated memory usage: 4 MB + + checkGroupInfo: false + + meetings: + validityPeriod: "48h" + + # To disable proteus for new federated conversations: + # federationProtocols: ["mls"] + + featureFlags: # see #RefConfigOptions in `/docs/reference` (https://github.com/wireapp/wire-server/) + appLock: + defaults: + config: + enforceAppLock: false + inactivityTimeoutSecs: 60 + status: enabled + classifiedDomains: + config: + domains: [] + status: disabled + conferenceCalling: + defaults: + status: enabled + lockStatus: locked + conversationGuestLinks: + defaults: + lockStatus: unlocked + status: enabled + fileSharing: + defaults: + lockStatus: unlocked + status: enabled + enforceFileDownloadLocation: + defaults: + lockStatus: locked + status: disabled + config: {} + legalhold: disabled-by-default + mls: + defaults: + status: disabled + config: + protocolToggleUsers: [] + defaultProtocol: proteus + allowedCipherSuites: [2] + defaultCipherSuite: 2 + supportedProtocols: [proteus, mls] # must contain defaultProtocol + groupInfoDiagnostics: false + lockStatus: unlocked + searchVisibilityInbound: + defaults: + status: disabled + selfDeletingMessages: + defaults: + config: + enforcedTimeoutSeconds: 0 + lockStatus: unlocked + status: enabled + sndFactorPasswordChallenge: + defaults: + lockStatus: locked + status: disabled + sso: disabled-by-default + teamSearchVisibility: disabled-by-default + validateSAMLemails: + defaults: + status: enabled + outlookCalIntegration: + defaults: + status: disabled + lockStatus: locked + mlsE2EId: + defaults: + status: disabled + config: + verificationExpiration: 86400 + acmeDiscoveryUrl: null + lockStatus: unlocked + mlsMigration: + defaults: + status: disabled + config: + startTime: null # "2029-05-16T10:11:12.123Z" + finaliseRegardlessAfter: null # "2029-10-17T00:00:00.000Z" + usersThreshold: 100 + clientsThreshold: 100 + lockStatus: locked + limitedEventFanout: + defaults: + status: disabled + domainRegistration: + defaults: + status: disabled + lockStatus: locked + channels: + defaults: + status: disabled + 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: + mlsConversationReset: false + assetAuditLog: + status: disabled + consumableNotifications: + defaults: + status: disabled + lockStatus: locked + chatBubbles: + defaults: + status: disabled + lockStatus: locked + apps: + defaults: + status: disabled + lockStatus: locked + simplifiedUserConnectionRequestQRCode: + defaults: + status: enabled + lockStatus: unlocked + stealthUsers: + defaults: + status: disabled + lockStatus: locked + meetings: + defaults: + status: enabled + lockStatus: unlocked + meetingsPremium: + defaults: + status: disabled + lockStatus: locked + aws: + region: "eu-west-1" + proxy: {} + serviceAccount: + # When setting this to 'false', either make sure that a service account named + # 'galley' exists or change the 'name' field to 'default' + create: true + name: galley + annotations: {} + automountServiceAccountToken: true + + secrets: {} + + podSecurityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + tests: + config: + {} + # uploadXml: + # baseUrl: s3://bucket/path/ + + secrets: + # uploadXmlAwsAccessKeyId: + # uploadXmlAwsSecretAccessKey: + + # These "secrets" are only used in tests and are therefore safe to be stored unencrypted + providerPrivateKey: | + -----BEGIN RSA PRIVATE KEY----- + MIIEpAIBAAKCAQEAu+Kg/PHHU3atXrUbKnw0G06FliXcNt3lMwl2os5twEDcPPFw + /feGiAKymxp+7JqZDrseS5D9THGrW+OQRIPHWvUBdiLfGrZqJO223DB6D8K2Su/o + dmnjZJ2z23rhXoEArTplu+Dg9K+c2LVeXTKVVPOaOzgtAB21XKRiQ4ermqgi3/nj + r03rXyq/qNkuNd6tNcg+HAfGxfGvvCSYBfiSbUKr/BeArYRcjzr/h5m1In6fG/if + 9GEI6m8dxHT9JbY53wiksowy6ajCuqskIFg87X883H+LA/d6X5CTiPv1VMxXdBUi + GPuC9IT/6CNQ1/LFt0P37ax58+LGYlaFo7lanQIDAQABAoIBAQC0doVy7zgpLsBv + Sz0AnbPe1pjxEwRlntRbJSfSULySALqJvs5s4adSVGUBHX3z/LousAP1SRpCppuU + 8wrLBFgjQVlaAzyQB84EEl+lNtrG8Jrvd2es9R/4sJDkqy50+yuPN5wnzWPFIjhg + 3jP5CHDu29y0LMzsY5yjkzDe9B0bueXEZVU+guRjhpwHHKOFeAr9J9bugFUwgeAr + jF0TztzFAb0fsUNPiQAho1J5PyjSVgItaPfAPv/p30ROG+rz+Rd5NSSvBC5F+yOo + azb84zzwCg/knAfIz7SOMRrmBh2qhGZFZ8gXdq65UaYv+cpT/qo28mpAT2vOkyeD + aPZp0ysBAoGBAOQROoDipe/5BTHBcXYuUE1qa4RIj3wgql5I8igXr4K6ppYBmaOg + DL2rrnqD86chv0P4l/XOomKFwYhVGXtqRkeYnk6mQXwNVkgqcGbY5PSNyMg5+ekq + jSOOPHGzzTWKzYuUDUpB/Lf6jbTv8fq2GYW3ZYiqQ/xiugOvglZrTE7NAoGBANLl + irjByfxAWGhzCrDx0x5MBpsetadI9wUA8u1BDdymsRg73FDn3z7NipVUAMDXMGVj + lqbCRlHESO2yP4GaPEA4FM+MbTZSuhAYV+SY07mEPLHF64/nJas83Zp91r5rhaqJ + L9rWCl3KJ5OUnr3YizCnHIW72FxjwtpjxHJLupsRAoGAGIbhy8qUHeKh9F/hW9xP + NoQjW+6Rv7+jktA1eqpRbbW1BJzXcQldVWiJMxPNuEOg1iZ98SlvvTi1P3wnaWZc + eIapP7wRfs3QYaJuxCC/Pq2g0ieqALFazGAXkALOJtvujvw1Ea9XBlIjuzmyxEuh + Iwg+Gxx0g0f6yTquwax4YGECgYEAnpAK3qKFNO1ECzQDo8oNy0ep59MNDPtlDhQK + katJus5xdCD9oq7TQKrVOTTxZAvmzTQ1PqfuqueDVYOhD9Zg2n/P1cRlEGTek99Z + pfvppB/yak6+r3FA9yBKFS/r1zuMQg3nNweav62QV/tz5pT7AdeDMGFtaPlwtTYx + qyWY5aECgYBPySbPccNj+xxQzxcti2y/UXjC04RgOA/Hm1D0exa0vBqS9uxlOdG8 + F47rKenpBrslvdfTVsCDB1xyP2ebWVzp6EqMycw6OLPxgo3fBfZ4pi6P+rByh0Cc + Lhfh+ET0CPnKCxtop3lUrn4ZvqchS0j3J+M0pDuqoWF5hfKxFhkEIw== + -----END RSA PRIVATE KEY----- + providerPublicKey: | + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu+Kg/PHHU3atXrUbKnw0 + G06FliXcNt3lMwl2os5twEDcPPFw/feGiAKymxp+7JqZDrseS5D9THGrW+OQRIPH + WvUBdiLfGrZqJO223DB6D8K2Su/odmnjZJ2z23rhXoEArTplu+Dg9K+c2LVeXTKV + VPOaOzgtAB21XKRiQ4ermqgi3/njr03rXyq/qNkuNd6tNcg+HAfGxfGvvCSYBfiS + bUKr/BeArYRcjzr/h5m1In6fG/if9GEI6m8dxHT9JbY53wiksowy6ajCuqskIFg8 + 7X883H+LA/d6X5CTiPv1VMxXdBUiGPuC9IT/6CNQ1/LFt0P37ax58+LGYlaFo7la + nQIDAQAB + -----END PUBLIC KEY----- + providerPublicCert: | + -----BEGIN CERTIFICATE----- + MIIDdjCCAl4CCQCm0AiwERR/qjANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJE + RTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xGDAWBgNVBAoMD1dp + cmUgU3dpc3MgR21iSDERMA8GA1UEAwwId2lyZS5jb20xHzAdBgkqhkiG9w0BCQEW + EGJhY2tlbmRAd2lyZS5jb20wHhcNMTYwODA0MTMxNDQyWhcNMzYwNzMwMTMxNDQy + WjB9MQswCQYDVQQGEwJERTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJs + aW4xGDAWBgNVBAoMD1dpcmUgU3dpc3MgR21iSDERMA8GA1UEAwwId2lyZS5jb20x + HzAdBgkqhkiG9w0BCQEWEGJhY2tlbmRAd2lyZS5jb20wggEiMA0GCSqGSIb3DQEB + AQUAA4IBDwAwggEKAoIBAQC74qD88cdTdq1etRsqfDQbToWWJdw23eUzCXaizm3A + QNw88XD994aIArKbGn7smpkOux5LkP1Mcatb45BEg8da9QF2It8atmok7bbcMHoP + wrZK7+h2aeNknbPbeuFegQCtOmW74OD0r5zYtV5dMpVU85o7OC0AHbVcpGJDh6ua + qCLf+eOvTetfKr+o2S413q01yD4cB8bF8a+8JJgF+JJtQqv8F4CthFyPOv+HmbUi + fp8b+J/0YQjqbx3EdP0ltjnfCKSyjDLpqMK6qyQgWDztfzzcf4sD93pfkJOI+/VU + zFd0FSIY+4L0hP/oI1DX8sW3Q/ftrHnz4sZiVoWjuVqdAgMBAAEwDQYJKoZIhvcN + AQELBQADggEBAEuwlHElIGR56KVC1dJiw238mDGjMfQzSP76Wi4zWS6/zZwJUuog + BkC+vacfju8UAMvL+vdqkjOVUHor84/2wuq0qn91AjOITD7tRAZB+XLXxsikKv/v + OXE3A/lCiNi882NegPyXAfFPp/71CIiTQZps1eQkAvhD5t5WiFYPESxDlvEJrHFY + XP4+pp8fL8YPS7iZNIq+z+P8yVIw+B/Hs0ht7wFIYN0xACbU8m9+Rs08JMoT16c+ + hZMuK3BWD3fzkQVfW0yMwz6fWRXB483ZmekGkgndOTDoJQMdJXZxHpI3t2FcxQYj + T45GXxRd18neXtuYa/OoAw9UQFDN5XfXN0g= + -----END CERTIFICATE----- + +cargohold: + replicaCount: 3 + image: + repository: quay.io/wire/cargohold + tag: do-not-use + service: + externalPort: 8080 + internalPort: 8080 + imagePullPolicy: "" + metrics: + serviceMonitor: + enabled: false + resources: + requests: + memory: "80Mi" + cpu: "100m" + limits: + memory: "200Mi" + config: + logLevel: Info + logFormat: StructuredJSON + logNetStrings: false + enableFederation: false # keep in sync with background-worker, brig and galley charts' config.enableFederation as well as wire-server chart's tags.federation + aws: + region: "eu-west-1" + s3Bucket: assets + # Multi-ingress configuration: + # multiIngress: + # - nginz-https.red.wire.example: assets.red.wire.example + # - nginz-https.green.wire.example: assets.green.wire.example + proxy: {} + settings: + maxTotalBytes: 104857632 # limit to 100 MiB + 32 bytes + maxTotalBytesStrict: 26214432 # limit to 25 MiB + 32 bytes + downloadLinkTTL: 300 # Seconds + assetAuditLogEnabled: false + # Disable one ore more API versions. Please make sure the configuration value is the same in all these charts: + # brig, cannon, cargohold, galley, gundeck, proxy, spar. + disabledAPIVersions: [development] + serviceAccount: + # When setting this to 'false', either make sure that a service account named + # 'cargohold' exists or change the 'name' field to 'default' + create: true + name: cargohold + annotations: {} + automountServiceAccountToken: true + secrets: {} + podSecurityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + tests: + config: {} + # config: + # uploadXml: + # baseUrl: s3://bucket/path/ + # secrets: + # uploadXmlAwsAccessKeyId: + # uploadXmlAwsSecretAccessKey: + +cannon: + replicaCount: 3 + image: + repository: quay.io/wire/cannon + tag: do-not-use + pullPolicy: IfNotPresent + # Optional extra arguments passed to cannon (e.g. ["+RTS", "-M2g", "-RTS"]) + cannonArgs: [] + nginzImage: + repository: quay.io/wire/nginz + tag: do-not-use + pullPolicy: IfNotPresent + config: + logLevel: Info + logFormat: StructuredJSON + logNetStrings: false + rabbitmq: + host: rabbitmq + port: 5672 + vHost: / + enableTls: false + insecureSkipVerifyTls: false + rabbitMqMaxConnections: 1000 + rabbitMqMaxChannels: 300 + cassandra: + host: aws-cassandra + # To enable TLS provide a CA: + # tlsCa: + # + # Or refer to an existing secret (containing the CA): + # tlsCaSecretRef: + # name: + # key: + + # See also the section 'Controlling the speed of websocket draining during + # cannon pod replacement' in docs/how-to/install/configuration-options.rst + drainOpts: + # The following drains a minimum of 400 connections/second + # for a total of 10000 over 25 seconds + # (if cannon holds more connections, draining will happen at a faster pace) + gracePeriodSeconds: 25 + millisecondsBetweenBatches: 50 + minBatchSize: 20 + + # TTL of stored notifications in Seconds. After this period, notifications + # will be deleted and thus not delivered. + # The default is 28 days. + notificationTTL: 2419200 + + # Disable one ore more API versions. Please make sure the configuration value is the same in all these charts: + # brig, cannon, cargohold, galley, gundeck, proxy, spar. + disabledAPIVersions: [development] + + metrics: + serviceMonitor: + enabled: false + + nginx_conf: + zauth_keystore: /etc/wire/nginz/secrets/zauth.conf + zauth_acl: /etc/wire/nginz/conf/zauth.acl + worker_processes: auto + worker_rlimit_nofile: 131072 + worker_connections: 65536 + disabled_paths: [] + rate_limit_reqs_per_user: "10r/s" + rate_limit_reqs_per_addr: "5r/m" + user_rate_limit_request_zones: [] + # The status code that is returned on throttling. + # 420 is the legacy status code and should be changed to 429 once all client support this + rate_limit_status: 420 + + tls: + protocols: TLSv1.2 TLSv1.3 + # NOTE: These are some sane defaults (compliant to TR-02102-2), you may want to overrride them on your own installation + # For TR-02102-2 see https://www.bsi.bund.de/SharedDocs/Downloads/EN/BSI/Publications/TechGuidelines/TG02102/BSI-TR-02102-2.html + # As a Wire employee, for Wire-internal discussions and context see + # * https://wearezeta.atlassian.net/browse/FS-33 + # * https://wearezeta.atlassian.net/browse/FS-444 + ciphers_tls12: "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384" + ciphers_tls13: "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384" + + # The origins from which we allow CORS requests. These are combined with + # 'external_env_domain' and 'additional_external_env_domains' to form a full + # url. + allowlisted_origins: + - webapp + - teams + - account + + # The origins from which we allow CORS requests at random ports. This is + # useful for testing with HTTP proxies and should not be used in production. + # The list entries must be full hostnames (they are **not** combined with + # 'external_env_domain'). http and https URLs are allow listed. + randomport_allowlisted_origins: [] # default is empty by intention + + # Configure multiple root domains for one backend. This is only advised in + # very specicial cases. Usually, this list should be empty. + additional_external_env_domains: [] + + # Setting this value does nothing as the only upstream recongnized here is + # 'cannon' and is forwarded to localhost. This is here only to make sure that + # nginx.conf templating doesn't differ too much with the one in nginz helm + # chart. + upstream_namespace: {} + + # Only upstream recognized by the generated nginx config is 'cannon', the + # server for this will be cannon running on localhost. This setting is like + # this so that templating for nginx.conf doesn't differ too much from the one + # in the nginz helm chart. + upstreams: + cannon: + - path: /await + envs: + - all + use_websockets: true + - path: /websocket + envs: + - all + use_websockets: true + - path: /events + envs: + - all + use_websockets: true + + # FUTUREWORK: allow resources for cannon and nginz to be different + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "1024Mi" + service: + name: cannon + internalPort: 8080 + externalPort: 8080 + nginz: + # Enable this only if service of `type: LoadBalancer` can work in your K8s + # cluster. + enabled: false + # hostname: # Needed when using either externalDNS or certManager + name: cannon-nginz + internalPort: 8443 + externalPort: 443 + annotations: {} + tls: + secretName: cannon-nginz-cert + externalDNS: + enabled: false + ttl: "10m" + certManager: + # When certManager is not enabled, certificates must be provided at + # .secrets.nginz.tls.crt and .secrets.nginz.tls.key. + enabled: false + certificate: + name: cannon-nginz + issuer: + name: letsencrypt + kind: ClusterIssuer + + podSecurityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + + # nodeSelector: + # wire.com/role: cannon + nodeSelector: {} + + # affinity: + # nodeAffinity: + # requiredDuringSchedulingIgnoredDuringExecution: + # nodeSelectorTerms: + # - matchExpressions: + # - key: wire.com/role + # operator: In + # values: + # - cannon + affinity: {} + + # tolerations: + # - key: "wire.com/role" + # operator: "Equal" + # value: "cannon" + # effect: "NoSchedule" + tolerations: [] + +proxy: + replicaCount: 3 + image: + repository: quay.io/wire/proxy + tag: do-not-use + service: + externalPort: 8080 + internalPort: 8080 + imagePullPolicy: "" + metrics: + serviceMonitor: + enabled: false + resources: + requests: + memory: "25Mi" + cpu: "50m" + limits: + memory: "50Mi" + config: + logLevel: Info + logFormat: StructuredJSON + logNetStrings: false + proxy: {} + # Disable one ore more API versions. Please make sure the configuration value is the same in all these charts: + # brig, cannon, cargohold, galley, gundeck, proxy, spar. + disabledAPIVersions: [development] + + podSecurityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + secrets: {} + +gundeck: + replicaCount: 3 + image: + repository: quay.io/wire/gundeck + tag: do-not-use + service: + externalPort: 8080 + internalPort: 8080 + imagePullPolicy: "" + metrics: + serviceMonitor: + enabled: false + resources: + requests: + memory: "300Mi" + cpu: "100m" + limits: + memory: "1Gi" + + # Should be greater than Warp's graceful shutdown (default 30s). + terminationGracePeriodSeconds: 40 + config: + logLevel: Info + logFormat: StructuredJSON + logNetStrings: false + rabbitmq: + host: rabbitmq + port: 5672 + vHost: / + enableTls: false + insecureSkipVerifyTls: false + cassandra: + host: aws-cassandra + # To enable TLS provide a CA: + # tlsCa: + # + # Or refer to an existing secret (containing the CA): + # tlsCaSecretRef: + # name: + # key: + redis: + host: redis-ephemeral + port: 6379 + connectionMode: "master" # master | cluster + enableTls: false + insecureSkipVerifyTls: false + # To configure custom TLS CA, please provide one of these: + # tlsCa: + # + # Or refer to an existing secret (containing the CA): + # tlsCaSecretRef: + # name: + # key: + + # To enable additional writes during a migration: + # redisAdditionalWrite: + # host: redis-two + # port: 6379 + # connectionMode: master + # enableTls: false + # insecureSkipVerifyTls: false + # + # # To configure custom TLS CA, please provide one of these: + # # tlsCa: + # # + # # Or refer to an existing secret (containing the CA): + # # tlsCaSecretRef: + # # name: + # # key: + aws: + region: "eu-west-1" + proxy: {} + # perNativePushConcurrency: 32 + maxConcurrentNativePushes: + soft: 1000 + # Disable one ore more API versions. Please make sure the configuration value is the same in all these charts: + # brig, cannon, cargohold, galley, gundeck, proxy, spar. + disabledAPIVersions: [development] + + # Maximum number of bytes loaded into memory when fetching (referenced) payloads. + # Gundeck will return a truncated page if the whole page's payload sizes would exceed this limit in total. + # Inlined payloads can cause greater payload sizes to be loaded into memory regardless of this setting. + # Tune this to trade off memory usage VS number of requests for gundeck workers. + maxPayloadLoadSize: 5242880 + # Cassandra page size for fetching notifications. Does not directly effect the + # page size request in the client API. A lower number will reduce the amount + # by which setMaxPayloadLoadSize is exceeded when loading notifications from + # the database if notifications have inlined payloads. + internalPageSize: 100 + + # TTL of stored notifications in Seconds. After this period, notifications + # will be deleted and thus not delivered. + # The default is 28 days. + notificationTTL: 2419200 + + # To enable cells notifications + # cellsEventQueue: cells_events + + serviceAccount: + # When setting this to 'false', either make sure that a service account named + # 'gundeck' exists or change the 'name' field to 'default' + create: true + name: gundeck + annotations: {} + automountServiceAccountToken: true + + secrets: {} + + podSecurityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + tests: + config: {} + # config: + # uploadXml: + # baseUrl: s3://bucket/path/ + # secrets: + # uploadXmlAwsAccessKeyId: + # uploadXmlAwsSecretAccessKey: + +spar: + replicaCount: 3 + image: + repository: quay.io/wire/spar + tag: do-not-use + imagePullPolicy: "" + metrics: + serviceMonitor: + enabled: false + resources: + requests: + memory: "100Mi" + cpu: "50m" + limits: + memory: "200Mi" + service: + externalPort: 8080 + internalPort: 8080 + config: + cassandra: + host: aws-cassandra + # To enable TLS provide a CA: + # tlsCa: + # + # Or refer to an existing secret (containing the CA): + # tlsCaSecretRef: + # name: + # key: + richInfoLimit: 5000 + maxScimTokens: 0 + + # Enables the `/sso/get-by-email` endpoint which returns the matching IdP for + # an email address of a team member. + enableIdPByEmailDiscovery: false + + logLevel: Info + logFormat: StructuredJSON + logNetStrings: false + maxttlAuthreq: 7200 + maxttlAuthresp: 7200 + proxy: {} + + # Disable one ore more API versions. Please make sure the configuration value is the same in all these charts: + # brig, cannon, cargohold, galley, gundeck, proxy, spar. + disabledAPIVersions: [development] + + # SAML - ServiceProvider configuration + # Usually, one would configure one set of options for a single domain. For + # multi-ingress setups (one backend is available through multiple domains), + # configure a map with the nginz domain as key. + # + # Single domain configuration + # appUri: # E.g. https://webapp. + # ssoUri: # E.g. https://nginz-https./sso + # contacts: + # - type: # One of ContactTechnical, ContactSupport, ContactAdministrative, ContactBilling, ContactOther + # company: # Optional + # email: # Optional + # givenName: # Optional + # surname: # Optional + # phone: # Optional + # + # Multi-ingress configuration + # domainConfigs: + # : # The domain of the incoming nginz-https host. E.g. nginz-https. + # appUri: # E.g. https://webapp. + # ssoUri: # E.g. https://nginz-https./sso + # contacts: + # - type: # One of ContactTechnical, ContactSupport, ContactAdministrative, ContactBilling, ContactOther + # company: # Optional + # email: # Optional + # givenName: # Optional + # surname: # Optional + # phone: # Optional + # : + # ... + + # SCIM base URI (by default deduced from single-domain spSsoUri, must be set + # for multi-ingress) + # scimBaseUri: + + podSecurityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + + tests: + config: {} + # config: + # uploadXml: + # baseUrl: s3://bucket/path/ + # secrets: + # uploadXmlAwsAccessKeyId: + # uploadXmlAwsSecretAccessKey: + +background-worker: + replicaCount: 1 + image: + repository: quay.io/wire/background-worker + tag: do-not-use + service: + internalPort: 8080 + externalPort: 8080 + # FUTUREWORK: Review these values when we have some experience + resources: + requests: + memory: "200Mi" + cpu: "100m" + limits: + memory: "512Mi" + metrics: + serviceMonitor: + enabled: false + config: + logLevel: Info + logFormat: StructuredJSON + enableFederation: false # keep in sync with brig, cargohold and galley charts' config.enableFederation as well as wire-server chart's tags.federation + brig: + host: brig + port: 8080 + galley: + host: galley + port: 8080 + gundeck: + host: gundeck + port: 8080 + federatorInternal: + host: federator + port: 8080 + spar: + host: spar + port: 8080 + rabbitmq: + host: rabbitmq + port: 5672 + vHost: / + adminPort: 15672 + enableTls: false + insecureSkipVerifyTls: false + # tlsCaSecretRef: + # name: + # key: + # Cassandra clusters used by background-worker + cassandra: + host: aws-cassandra + cassandraGalley: + host: aws-cassandra + cassandraBrig: + host: aws-cassandra + + # Postgres connection settings + # + # Values are described in https://www.postgresql.org/docs/17/libpq-connect.html#LIBPQ-PARAMKEYWORDS + # To set the password via a brig secret see `secrets.pgPassword`. + # + # `additionalVolumeMounts` and `additionalVolumes` can be used to mount + # additional files (e.g. certificates) into the brig container. This way + # does not work for password files (parameter `passfile`), because + # libpq-connect requires access rights (mask 0600) for them that we cannot + # provide for random uids. + # + # Below is an example configuration we're using for our CI tests. + postgresql: + host: postgresql # DNS name without protocol + port: "5432" + user: wire-server + dbname: wire-server + postgresqlPool: + size: 5 + acquisitionTimeout: 10s + agingTimeout: 1d + idlenessTimeout: 10m + + # Setting this to `true` will start conversation migration to postgresql. + # + # NOTE: It is very important that galley be configured to with + # `settings.postgresMigration.conversation` with `migration-to-postgresql` + # before setting this to `true`. + migrateConversations: false + migrateConversationsOptions: + pageSize: 10000 + parallelism: 2 + # This will start the migration of conversation codes. + # It's important to set `settings.postgresMigration.conversationCodes` to `migration-to-postgresql` + # before starting the migration. + migrateConversationCodes: false + # This will start the migration of team features. + # It's important to set `settings.postgresMigration.teamFeatures` to `migration-to-postgresql` + # before starting the migration. + migrateTeamFeatures: false + + backendNotificationPusher: + pushBackoffMinWait: 10000 # in microseconds, so 10ms + pushBackoffMaxWait: 300000000 # microseconds, so 300s + remotesRefreshInterval: 300000000 # microseconds, so 300s + + # Background jobs consumer configuration + backgroundJobs: + # Maximum number of in-flight jobs per process + concurrency: 8 + # Per-attempt timeout in seconds + jobTimeout: 60s + # Total attempts, including the first try + maxAttempts: 3 + + # Controls where conversation data is stored/accessed + postgresMigration: + conversation: cassandra + conversationCodes: cassandra + teamFeatures: cassandra + + secrets: + {} + # pgPassword: + + podSecurityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + +brig: + replicaCount: 3 + image: + repository: quay.io/wire/brig + tag: do-not-use + service: + externalPort: 8080 + internalPort: 8080 + resources: + requests: + memory: "200Mi" + cpu: "100m" + limits: + memory: "512Mi" + metrics: + serviceMonitor: + enabled: false + livenessProbe: + # Maximum allowed network connections before forcing restart (mitigates connection/memory leaks) + maxConnections: 500 + # This is not supported for production use, only here for testing: + # preStop: + # exec: + # command: ["sh", "-c", "curl http://acme.example"] + config: + logLevel: Info + logFormat: StructuredJSON + logNetStrings: false + cassandra: + host: aws-cassandra + # To enable TLS provide a CA: + # tlsCa: + # + # Or refer to an existing secret (containing the CA): + # tlsCaSecretRef: + # name: + # key: + + elasticsearch: + scheme: http + host: elasticsearch-client + port: 9200 + index: directory + insecureSkipVerifyTls: false + # To configure custom TLS CA, please provide one of these: + # tlsCa: + # + # Or refer to an existing secret (containing the CA): + # tlsCaSecretRef: + # name: + # key: + additionalWriteScheme: http + # additionalWriteHost: + additionalWritePort: 9200 + # additionalWriteIndex: + additionalInsecureSkipVerifyTls: false + # To configure custom TLS CA, please provide one of these: + # additionalTlsCa: + # + # Or refer to an existing secret (containing the CA): + # additionalTlsCaSecretRef: + # name: + # key: + aws: + region: "eu-west-1" + sesEndpoint: https://email.eu-west-1.amazonaws.com + sqsEndpoint: https://sqs.eu-west-1.amazonaws.com + # dynamoDBEndpoint: https://dynamodb.eu-west-1.amazonaws.com + # -- If set to false, 'dynamoDBEndpoint' _must_ be set. + randomPrekeys: true + useSES: true + 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 + rabbitmq: + host: rabbitmq + port: 5672 + vHost: / + enableTls: false + insecureSkipVerifyTls: false + # tlsCaSecretRef: + # name: + # key: + + # Postgres connection settings + # + # Values are described in https://www.postgresql.org/docs/17/libpq-connect.html#LIBPQ-PARAMKEYWORDS + # To set the password via a brig secret see `secrets.pgPassword`. + # + # `additionalVolumeMounts` and `additionalVolumes` can be used to mount + # additional files (e.g. certificates) into the brig container. This way + # does not work for password files (parameter `passfile`), because + # libpq-connect requires access rights (mask 0600) for them that we cannot + # provide for random uids. + # + # Below is an example configuration we're using for our CI tests. + postgresql: + host: postgresql # DNS name without protocol + port: "5432" + user: wire-server + dbname: wire-server + postgresqlPool: + size: 100 + acquisitionTimeout: 10s + agingTimeout: 1d + idlenessTimeout: 10m + + emailSMS: + general: + templateBranding: + brand: Wire + brandUrl: https://wire.com + brandLabelUrl: wire.com + brandLogoUrl: https://wire.com/p/img/email/logo-email-black.png + brandService: Wire Service Provider + copyright: © WIRE SWISS GmbH + misuse: misuse@wire.com + legal: https://wire.com/legal/ + forgot: https://wire.com/forgot/ + support: https://support.wire.com/ + authSettings: + keyIndex: 1 + userTokenTimeout: 4838400 + sessionTokenTimeout: 86400 + accessTokenTimeout: 900 + providerTokenTimeout: 900 + legalholdUserTokenTimeout: 4838400 + legalholdAccessTokenTimeout: 900 + # sft: + # sftBaseDomain: sft.wire.example.com + # sftSRVServiceName: sft + # sftDiscoveryIntervalSeconds: 10 + # sftListLength: 20 + # sftToken: + # ttl: 120 + # secret: /etc/wire/brig/secrets/sftTokenSecret # this is the default path for secret.sftTokenSecret + optSettings: + setActivationTimeout: 1209600 + setTeamInvitationTimeout: 1814400 + setUserMaxConnections: 1000 + setCookieInsecure: false + setUserCookieRenewAge: 1209600 + setUserCookieLimit: 32 + setUserCookieThrottle: + stdDev: 3000 + retryAfter: 86400 + setRichInfoLimit: 5000 + setDefaultUserLocale: en + setMaxTeamSize: 10000 + setMaxConvSize: 500 + # Allowed values: https://github.com/wireapp/wire-server/blob/0126651a25aabc0c5589edc2b1988bb06550a03a/services/brig/src/Brig/Options.hs#L304-L306 + # Description: https://github.com/wireapp/wire-server/blob/0126651a25aabc0c5589edc2b1988bb06550a03a/services/brig/src/Brig/Options.hs#L290-L299 + setEmailVisibility: visible_to_self + setPropertyMaxKeyLen: 1024 + setPropertyMaxValueLen: 524288 + setDeleteThrottleMillis: 100 + # Allow search within same team only. Default: false + # setSearchSameTeamOnly: false|true + # Set max number of user clients. Default: 7 + # setUserMaxPermClients: + # Customer extensions. If this is not part of your contract with wire, use at your own risk! + # Details: https://github.com/wireapp/wire-server/blob/3d5684023c54fe580ab27c11d7dae8f19a29ddbc/services/brig/src/Brig/Options.hs#L465-L503 + # setCustomerExtensions: + # domainsBlockedForRegistration: + # - wire.example + set2FACodeGenerationDelaySecs: 300 # 5 minutes + setNonceTtlSecs: 300 # 5 minutes + setDpopMaxSkewSecs: 1 + setDpopTokenExpirationTimeSecs: 300 # 5 minutes + setOAuthAuthCodeExpirationTimeSecs: 300 # 5 minutes + setOAuthAccessTokenExpirationTimeSecs: 900 # 15 minutes + setOAuthRefreshTokenExpirationTimeSecs: 14515200 # 24 weeks + setOAuthEnabled: true + setOAuthMaxActiveRefreshTokens: 10 + # Disable one ore more API versions. Please make sure the configuration value is the same in all these charts: + # brig, cannon, cargohold, galley, gundeck, proxy, spar. + setDisabledAPIVersions: [development] + setFederationStrategy: allowNone + setFederationDomainConfigsUpdateFreq: 10 + setPasswordHashingOptions: + algorithm: scrypt # or argon2id + # When algorithm is argon2id, these can be configured: + # iterations: + # parallelism: + # memory: + setPasswordHashingRateLimit: + ipAddrLimit: + burst: 5 + inverseRate: 300000000 # 5 mins, makes it 12 reqs/hour + userLimit: + burst: 5 + inverseRate: 60000000 # 1 min, makes it 60 req/hour + internalLimit: + burst: 10 + inverseRate: 0 # No rate limiting for internal use + ipv4CidrBlock: 32 # Only block individual IP addresses + ipv6CidrBlock: 64 # Block /64 range at a time. + ipAddressExceptions: [] + maxRateLimitedKeys: 100000 # Estimated memory usage: 4 MB + # setAuditLogEmailRecipient: security@wire.com + setEphemeralUserCreationEnabled: true + + smtp: + passwordFile: /etc/wire/brig/secrets/smtp-password.txt + proxy: {} + wireServerEnterprise: + enabled: false + + turnStatic: + v1: + - turn:localhost:3478 + v2: + - turn:localhost:3478 + - turn:localhost:3478?transport=tcp + + turn: + serversSource: files # files | dns + # baseDomain: turn.wire.example # Must be configured if serversSource is dns + discoveryIntervalSeconds: 10 # Used only if serversSource is dns + + serviceAccount: + # When setting this to 'false', either make sure that a service account named + # 'brig' exists or change the 'name' field to 'default' + create: true + name: brig + annotations: {} + automountServiceAccountToken: true + + secrets: {} + + podSecurityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + tests: + config: + {} + # uploadXml: + # baseUrl: s3://bucket/path/ + + secrets: + # uploadXmlAwsAccessKeyId: + # uploadXmlAwsSecretAccessKey: + + # These "secrets" are only used in tests and are therefore safe to be stored unencrypted + providerPrivateKey: | + -----BEGIN RSA PRIVATE KEY----- + MIIEpAIBAAKCAQEAu+Kg/PHHU3atXrUbKnw0G06FliXcNt3lMwl2os5twEDcPPFw + /feGiAKymxp+7JqZDrseS5D9THGrW+OQRIPHWvUBdiLfGrZqJO223DB6D8K2Su/o + dmnjZJ2z23rhXoEArTplu+Dg9K+c2LVeXTKVVPOaOzgtAB21XKRiQ4ermqgi3/nj + r03rXyq/qNkuNd6tNcg+HAfGxfGvvCSYBfiSbUKr/BeArYRcjzr/h5m1In6fG/if + 9GEI6m8dxHT9JbY53wiksowy6ajCuqskIFg87X883H+LA/d6X5CTiPv1VMxXdBUi + GPuC9IT/6CNQ1/LFt0P37ax58+LGYlaFo7lanQIDAQABAoIBAQC0doVy7zgpLsBv + Sz0AnbPe1pjxEwRlntRbJSfSULySALqJvs5s4adSVGUBHX3z/LousAP1SRpCppuU + 8wrLBFgjQVlaAzyQB84EEl+lNtrG8Jrvd2es9R/4sJDkqy50+yuPN5wnzWPFIjhg + 3jP5CHDu29y0LMzsY5yjkzDe9B0bueXEZVU+guRjhpwHHKOFeAr9J9bugFUwgeAr + jF0TztzFAb0fsUNPiQAho1J5PyjSVgItaPfAPv/p30ROG+rz+Rd5NSSvBC5F+yOo + azb84zzwCg/knAfIz7SOMRrmBh2qhGZFZ8gXdq65UaYv+cpT/qo28mpAT2vOkyeD + aPZp0ysBAoGBAOQROoDipe/5BTHBcXYuUE1qa4RIj3wgql5I8igXr4K6ppYBmaOg + DL2rrnqD86chv0P4l/XOomKFwYhVGXtqRkeYnk6mQXwNVkgqcGbY5PSNyMg5+ekq + jSOOPHGzzTWKzYuUDUpB/Lf6jbTv8fq2GYW3ZYiqQ/xiugOvglZrTE7NAoGBANLl + irjByfxAWGhzCrDx0x5MBpsetadI9wUA8u1BDdymsRg73FDn3z7NipVUAMDXMGVj + lqbCRlHESO2yP4GaPEA4FM+MbTZSuhAYV+SY07mEPLHF64/nJas83Zp91r5rhaqJ + L9rWCl3KJ5OUnr3YizCnHIW72FxjwtpjxHJLupsRAoGAGIbhy8qUHeKh9F/hW9xP + NoQjW+6Rv7+jktA1eqpRbbW1BJzXcQldVWiJMxPNuEOg1iZ98SlvvTi1P3wnaWZc + eIapP7wRfs3QYaJuxCC/Pq2g0ieqALFazGAXkALOJtvujvw1Ea9XBlIjuzmyxEuh + Iwg+Gxx0g0f6yTquwax4YGECgYEAnpAK3qKFNO1ECzQDo8oNy0ep59MNDPtlDhQK + katJus5xdCD9oq7TQKrVOTTxZAvmzTQ1PqfuqueDVYOhD9Zg2n/P1cRlEGTek99Z + pfvppB/yak6+r3FA9yBKFS/r1zuMQg3nNweav62QV/tz5pT7AdeDMGFtaPlwtTYx + qyWY5aECgYBPySbPccNj+xxQzxcti2y/UXjC04RgOA/Hm1D0exa0vBqS9uxlOdG8 + F47rKenpBrslvdfTVsCDB1xyP2ebWVzp6EqMycw6OLPxgo3fBfZ4pi6P+rByh0Cc + Lhfh+ET0CPnKCxtop3lUrn4ZvqchS0j3J+M0pDuqoWF5hfKxFhkEIw== + -----END RSA PRIVATE KEY----- + providerPublicKey: | + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu+Kg/PHHU3atXrUbKnw0 + G06FliXcNt3lMwl2os5twEDcPPFw/feGiAKymxp+7JqZDrseS5D9THGrW+OQRIPH + WvUBdiLfGrZqJO223DB6D8K2Su/odmnjZJ2z23rhXoEArTplu+Dg9K+c2LVeXTKV + VPOaOzgtAB21XKRiQ4ermqgi3/njr03rXyq/qNkuNd6tNcg+HAfGxfGvvCSYBfiS + bUKr/BeArYRcjzr/h5m1In6fG/if9GEI6m8dxHT9JbY53wiksowy6ajCuqskIFg8 + 7X883H+LA/d6X5CTiPv1VMxXdBUiGPuC9IT/6CNQ1/LFt0P37ax58+LGYlaFo7la + nQIDAQAB + -----END PUBLIC KEY----- + providerPublicCert: | + -----BEGIN CERTIFICATE----- + MIIDdjCCAl4CCQCm0AiwERR/qjANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJE + RTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xGDAWBgNVBAoMD1dp + cmUgU3dpc3MgR21iSDERMA8GA1UEAwwId2lyZS5jb20xHzAdBgkqhkiG9w0BCQEW + EGJhY2tlbmRAd2lyZS5jb20wHhcNMTYwODA0MTMxNDQyWhcNMzYwNzMwMTMxNDQy + WjB9MQswCQYDVQQGEwJERTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJs + aW4xGDAWBgNVBAoMD1dpcmUgU3dpc3MgR21iSDERMA8GA1UEAwwId2lyZS5jb20x + HzAdBgkqhkiG9w0BCQEWEGJhY2tlbmRAd2lyZS5jb20wggEiMA0GCSqGSIb3DQEB + AQUAA4IBDwAwggEKAoIBAQC74qD88cdTdq1etRsqfDQbToWWJdw23eUzCXaizm3A + QNw88XD994aIArKbGn7smpkOux5LkP1Mcatb45BEg8da9QF2It8atmok7bbcMHoP + wrZK7+h2aeNknbPbeuFegQCtOmW74OD0r5zYtV5dMpVU85o7OC0AHbVcpGJDh6ua + qCLf+eOvTetfKr+o2S413q01yD4cB8bF8a+8JJgF+JJtQqv8F4CthFyPOv+HmbUi + fp8b+J/0YQjqbx3EdP0ltjnfCKSyjDLpqMK6qyQgWDztfzzcf4sD93pfkJOI+/VU + zFd0FSIY+4L0hP/oI1DX8sW3Q/ftrHnz4sZiVoWjuVqdAgMBAAEwDQYJKoZIhvcN + AQELBQADggEBAEuwlHElIGR56KVC1dJiw238mDGjMfQzSP76Wi4zWS6/zZwJUuog + BkC+vacfju8UAMvL+vdqkjOVUHor84/2wuq0qn91AjOITD7tRAZB+XLXxsikKv/v + OXE3A/lCiNi882NegPyXAfFPp/71CIiTQZps1eQkAvhD5t5WiFYPESxDlvEJrHFY + XP4+pp8fL8YPS7iZNIq+z+P8yVIw+B/Hs0ht7wFIYN0xACbU8m9+Rs08JMoT16c+ + hZMuK3BWD3fzkQVfW0yMwz6fWRXB483ZmekGkgndOTDoJQMdJXZxHpI3t2FcxQYj + T45GXxRd18neXtuYa/OoAw9UQFDN5XfXN0g= + -----END CERTIFICATE----- + + # pgPassword: + test: + elasticsearch: + additionalHost: elasticsearch-ephemeral diff --git a/deploy/dockerephemeral/db-migrate/brig-index.sh b/deploy/dockerephemeral/db-migrate/brig-index.sh index 32cf6ed8783..81a459292be 100755 --- a/deploy/dockerephemeral/db-migrate/brig-index.sh +++ b/deploy/dockerephemeral/db-migrate/brig-index.sh @@ -1,7 +1,9 @@ #!/usr/bin/env sh until_ready() { - until $1; do echo 'service not ready yet'; sleep 5; done + cmd=$1 + until $cmd; do echo 'service not ready yet'; sleep 5; done + return 0 } until_ready "brig-index reset --elasticsearch-server http://elasticsearch:9200" diff --git a/deploy/dockerephemeral/db-migrate/brig-schema.sh b/deploy/dockerephemeral/db-migrate/brig-schema.sh index 0d0da62f829..5d8bcad70be 100755 --- a/deploy/dockerephemeral/db-migrate/brig-schema.sh +++ b/deploy/dockerephemeral/db-migrate/brig-schema.sh @@ -1,7 +1,9 @@ #!/usr/bin/env sh until_ready() { - until $1; do echo 'service not ready yet'; sleep 5; done + cmd=$1 + until $cmd; do echo 'service not ready yet'; sleep 5; done + return 0 } until_ready "brig-schema --host cassandra --keyspace brig_test --replication-factor 1" diff --git a/deploy/dockerephemeral/db-migrate/galley-schema.sh b/deploy/dockerephemeral/db-migrate/galley-schema.sh index acc75b7df5b..e5b0da7dd22 100755 --- a/deploy/dockerephemeral/db-migrate/galley-schema.sh +++ b/deploy/dockerephemeral/db-migrate/galley-schema.sh @@ -1,7 +1,9 @@ #!/usr/bin/env sh until_ready() { - until $1; do echo 'service not ready yet'; sleep 5; done + cmd=$1 + until $cmd; do echo 'service not ready yet'; sleep 5; done + return 0 } until_ready "galley-schema --host cassandra --keyspace galley_test --replication-factor 1" diff --git a/deploy/dockerephemeral/db-migrate/gundeck-schema.sh b/deploy/dockerephemeral/db-migrate/gundeck-schema.sh index e6488f06c40..55a93a0263b 100755 --- a/deploy/dockerephemeral/db-migrate/gundeck-schema.sh +++ b/deploy/dockerephemeral/db-migrate/gundeck-schema.sh @@ -1,7 +1,9 @@ #!/usr/bin/env sh until_ready() { - until $1; do echo 'service not ready yet'; sleep 5; done + cmd=$1 + until $cmd; do echo 'service not ready yet'; sleep 5; done + return 0 } until_ready "gundeck-schema --host cassandra --keyspace gundeck_test --replication-factor 1" diff --git a/deploy/dockerephemeral/db-migrate/spar-schema.sh b/deploy/dockerephemeral/db-migrate/spar-schema.sh index a67470793be..9f82ce1bc45 100755 --- a/deploy/dockerephemeral/db-migrate/spar-schema.sh +++ b/deploy/dockerephemeral/db-migrate/spar-schema.sh @@ -1,7 +1,9 @@ #!/usr/bin/env sh until_ready() { - until $1; do echo 'service not ready yet'; sleep 5; done + cmd=$1 + until $cmd; do echo 'service not ready yet'; sleep 5; done + return 0 } until_ready "spar-schema --host cassandra --keyspace spar_test --replication-factor 1" diff --git a/deploy/dockerephemeral/init.sh b/deploy/dockerephemeral/init.sh index d80a7cb4c1f..42b64689edd 100755 --- a/deploy/dockerephemeral/init.sh +++ b/deploy/dockerephemeral/init.sh @@ -1,7 +1,9 @@ #!/usr/bin/env sh exec_until_ready() { - until $1; do echo 'service not ready yet'; sleep 1; done + cmd=$1 + until $cmd; do echo 'service not ready yet'; sleep 1; done + return 0 } # Assumes this to be run in an environment with `aws` installed diff --git a/deploy/dockerephemeral/init_vhosts.sh b/deploy/dockerephemeral/init_vhosts.sh index 425e783d34c..3e123591ef6 100755 --- a/deploy/dockerephemeral/init_vhosts.sh +++ b/deploy/dockerephemeral/init_vhosts.sh @@ -1,14 +1,15 @@ #!/usr/bin/env sh exec_until_ready() { - until $1; do - echo 'service not ready yet' - sleep 1 - done + cmd=$1 + until $cmd; do echo 'service not ready yet'; sleep 1; done + return 0 } create_vhost() { - exec_until_ready "curl --cacert /etc/rabbitmq-ca.pem -u $RABBITMQ_USERNAME:$RABBITMQ_PASSWORD -X PUT https://rabbitmq:15671/api/vhosts/$1" + vhost="$1" + exec_until_ready "curl --cacert /etc/rabbitmq-ca.pem -u $RABBITMQ_USERNAME:$RABBITMQ_PASSWORD -X PUT https://rabbitmq:15671/api/vhosts/$vhost" + return 0 } echo 'Creating RabbitMQ resources' diff --git a/deploy/dockerephemeral/run.sh b/deploy/dockerephemeral/run.sh index 1324d8b3406..bbb51d13370 100755 --- a/deploy/dockerephemeral/run.sh +++ b/deploy/dockerephemeral/run.sh @@ -10,7 +10,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DOCKER_FILE="$SCRIPT_DIR/docker-compose.yaml" FED_VERSIONS=(0 1 2) -if [ -e "${SCRIPT_DIR}/run.before.hook.local" ]; then +if [[ -e "${SCRIPT_DIR}/run.before.hook.local" ]]; then # shellcheck disable=SC1091 . "${SCRIPT_DIR}/run.before.hook.local" fi @@ -25,15 +25,17 @@ done dc() { docker-compose "${opts[@]}" "$@" + return 0 } cleanup() { dc down + return 0 } -if [ -z "$1" ]; then +if [[ -z "$1" ]]; then dc up -d - if [ -e "${SCRIPT_DIR}/run.after.hook.local" ]; then + if [[ -e "${SCRIPT_DIR}/run.after.hook.local" ]]; then # shellcheck disable=SC1091 . "${SCRIPT_DIR}/run.after.hook.local" fi diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index 911594aac37..edda1f74f53 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -288,19 +288,21 @@ The lock status for individual teams can be changed via the internal API (`PUT / The feature status for individual teams can be changed via the public API (if the feature is unlocked). -### Validate SAML Emails +### Require External Email Verification -The feature only affects email address changes originating from SCIM or SAML. Personal users and team users provisioned through the team management app will *always* be validated. +The external feature name `validateSAMLemails` is kept for backward compatibility, but it is misleading: the feature applies to email addresses originating from both SCIM and SAML, and it controls ownership verification rather than generic email validation. -`enabled` means "user has authority over email address": if a new user account with an email address is created, the user behind the account will receive a validation email. If they follow the validation procedure, they will be able to receive emails about their account, eg., if a new device is associated with the account. If the user does not validate their email address, they can still use it to login. +The feature only affects email address changes originating from SCIM or SAML. Personal users and team users provisioned through the team management app will *always* go through email verification. -`disabled` means "team admin has authority over email address, and by extension over all member accounts": if a user account with an email address is created, the address is considered valid immediately, without any emails being sent out, and without confirmation from the recipient. +`enabled` means "user has authority over email address": if a new user account with an email address is created, the user behind the account will receive a verification email. If they complete the verification flow, they will be able to receive emails about their account, eg., if a new device is associated with the account. If they do not verify their email address, they can still use it to log in. -Validate SAML emails is enabled by default. To disable, use the following syntax: +`disabled` means "team admin has authority over email address, and by extension over all member accounts": if a user account with an email address is created, the address is auto-activated immediately, without any verification email being sent and without confirmation from the recipient. The user can still receive later account notifications on that address, eg., if a new device is associated with the account. + +This feature is enabled by default. To disable it, use the following syntax: ```yaml # galley.yaml -validateSAMLEmails: +validateSAMLemails: defaults: status: disabled ``` @@ -2000,6 +2002,23 @@ gundeck: settings: cellsEventQueue: "cells_events" ``` + +## Configure consumable notifications + +The `consumableNotifications` flag controls whether the RabbitMQ-backed Events +API for clients with the `consumable-notifications` capability is operational. +When disabled, the legacy notification flow remains active. + +This is a root-level Helm `values.yaml` setting. It is rendered into both the +`brig` and `gundeck` service configs: + +```yaml +consumableNotifications: false +``` + +- In `brig`, it is rendered as `optSettings.setConsumableNotifications`. +- In `gundeck`, it is rendered as `settings.consumableNotifications`. + ## Background worker: Background jobs The background worker processes asynchronous jobs (conversation migrations, backend notifications). Configuration is supplied via Helm under `background-worker.config` and rendered into `background-worker.yaml`. diff --git a/docs/src/how-to/install/infrastructure-configuration.md b/docs/src/how-to/install/infrastructure-configuration.md index 337917dac5a..34e1eb2c19d 100644 --- a/docs/src/how-to/install/infrastructure-configuration.md +++ b/docs/src/how-to/install/infrastructure-configuration.md @@ -330,82 +330,6 @@ As of 2020-08-10, the documentation sections below are partially out of date and - Add password in `secrets/wire-server`’s secrets file under `brig.secrets.smtpPassword` -## Load balancer on bare metal servers - -**Assumptions**: - -- You installed kubernetes on bare metal servers or virtual machines - that can bind to a public IP address. -- **If you are using AWS or another cloud provider, see**[Creating a - cloudprovider-based load - balancer]()**instead** - -**Provides**: - -- Allows using a provided Load balancer for incoming traffic -- SSL termination is done on the ingress controller -- You can access your wire-server backend with given DNS names, over - SSL and from anywhere in the internet - -**You need**: - -- A kubernetes node with a *public* IP address (or internal, if you do - not plan to expose the Wire backend over the Internet but we will - assume you are using a public IP address) -- DNS records for the different exposed addresses (the ingress depends - on the usage of virtual hosts), namely: - - `nginz-https.` - - `nginz-ssl.` - - `assets.` - - `webapp.` - - `account.` - - `teams.` -- A wildcard certificate for the different hosts (`*.`) - we - assume you want to do SSL termination on the ingress controller - -**Caveats**: - -- Note that there can be only a *single* load balancer, otherwise your - cluster might become - [unstable](https://metallb.universe.tf/installation/) - -**How to configure**: - -```default -cp values/metallb/demo-values.example.yaml values/metallb/demo-values.yaml -cp values/nginx-ingress-services/demo-values.example.yaml values/nginx-ingress-services/demo-values.yaml -cp values/nginx-ingress-services/demo-secrets.example.yaml values/nginx-ingress-services/demo-secrets.yaml -``` - -- Adapt `values/metallb/demo-values.yaml` to provide a list of public - IP address CIDRs that your kubernetes nodes can bind to. -- Adapt `values/nginx-ingress-services/demo-values.yaml` with correct URLs -- Put your TLS cert and key into - `values/nginx-ingress-services/demo-secrets.yaml`. - -Install `metallb` (for more information see the -[docs](https://metallb.universe.tf)): - -```sh -helm upgrade --install --namespace metallb-system metallb wire/metallb \ - -f values/metallb/demo-values.yaml \ - --wait --timeout 1800 -``` - -Install `ingress-nginx-controller` (`nginx-ingress` controller) and -`nginx-ingress-services`: - -:: -: helm upgrade –install –namespace demo demo-ingress-nginx-controller wire/ingress-nginx-controller - -: –wait - -helm upgrade –install –namespace demo demo-nginx-ingress-services wire/nginx-ingress-services - -: -f values/nginx-ingress-services/demo-values.yaml -f values/nginx-ingress-services/demo-secrets.yaml –wait - -Now, create DNS records for the URLs configured above. - ## Load Balancer on cloud-provider ### AWS diff --git a/flake.lock b/flake.lock index 1d17c5750d5..0d4742fa79f 100644 --- a/flake.lock +++ b/flake.lock @@ -153,6 +153,22 @@ "type": "github" } }, + "nixpkgs-unstable": { + "locked": { + "lastModified": 1772963539, + "narHash": "sha256-9jVDGZnvCckTGdYT53d/EfznygLskyLQXYwJLKMPsZs=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "9dcb002ca1690658be4a04645215baea8b95f31d", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, "nixpkgs_24_11": { "locked": { "lastModified": 1751274312, @@ -197,6 +213,7 @@ "hspec-wai": "hspec-wai", "http-client": "http-client", "nixpkgs": "nixpkgs", + "nixpkgs-unstable": "nixpkgs-unstable", "nixpkgs_24_11": "nixpkgs_24_11", "postie": "postie", "servant-openapi3": "servant-openapi3", diff --git a/flake.nix b/flake.nix index 070afbd9a40..0819c0d85dc 100644 --- a/flake.nix +++ b/flake.nix @@ -5,6 +5,7 @@ self.submodules = true; nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-25.11"; nixpkgs_24_11.url = "github:nixos/nixpkgs?ref=nixos-24.11"; + nixpkgs-unstable.url = "github:nixos/nixpkgs?ref=nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; tom-bombadil = { url = "github:wireapp/tom-bombadil"; @@ -82,7 +83,7 @@ }; }; - outputs = inputs@{ nixpkgs, nixpkgs_24_11, flake-utils, tom-bombadil, ... }: + outputs = inputs@{ nixpkgs, nixpkgs_24_11, nixpkgs-unstable, flake-utils, tom-bombadil, ... }: flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { @@ -95,6 +96,9 @@ pkgs_24_11 = import nixpkgs_24_11 { inherit system; }; + pkgs_unstable = import nixpkgs-unstable { + inherit system; + }; bomDependenciesDrv = tom-bombadil.lib.${system}.bomDependenciesDrv; wireServerPkgs = import ./nix { inherit pkgs pkgs_24_11 inputs bomDependenciesDrv; }; in @@ -104,7 +108,41 @@ inherit (wireServerPkgs) pkgs profileEnv wireServer docs docsEnv mls-test-cli nginz; }; devShells = { - default = wireServerPkgs.wireServer.devEnv; + default = pkgs.mkShell { + packages = wireServerPkgs.wireServer.devEnvPkgs; + }; + }; + # Shell environment for generating Software Bill of Materials (SBOMs) + # Used on CI. + devShells.sbom = pkgs.mkShell { + packages = [ + # Shell and core utilities + pkgs.bash + pkgs.coreutils + pkgs.findutils + pkgs.git + + # JSON/YAML processing + pkgs.jq + pkgs.yq + + # Network tools + pkgs.curl + + # Container and SBOM tools + pkgs.cyclonedx-cli + pkgs_unstable.syft + pkgs.kubernetes-helm + pkgs.helmfile + ] ++ pkgs.lib.optionals pkgs.stdenv.isLinux [ + # Linux-only container tools + pkgs.skopeo + pkgs.docker + pkgs.docker-compose + ]; + meta = { + description = "Development shell with tools for SBOM generation"; + }; }; } ); diff --git a/hack/bin/cabal-run-integration.sh b/hack/bin/cabal-run-integration.sh index 4b14a36a1f9..56b11768cc1 100755 --- a/hack/bin/cabal-run-integration.sh +++ b/hack/bin/cabal-run-integration.sh @@ -62,21 +62,24 @@ run_integration_tests() { -i "$TOP_LEVEL/services/integration.yaml" \ "${@:2}" fi + + return 0 } run_all_integration_tests() { for d in "$TOP_LEVEL/services/"*/; do package=$(basename "$d") service_dir="$TOP_LEVEL/services/$package" - if [ -d "$service_dir/test/integration" ] || [ -d "$service_dir/test-integration" ]; then + if [[ -d "$service_dir/test/integration" ]] || [[ -d "$service_dir/test-integration" ]]; then run_integration_tests "$package" fi done run_integration_tests "stern" + return 0 } -if [ "$package" == "all" ]; then - if [ -n "${2:-}" ]; then +if [[ "$package" == "all" ]]; then + if [[ -n "${2:-}" ]]; then echo -e "\e[31mCannot pass additional args to all integrations tests.\e[0m" >&2 exit 1 fi diff --git a/hack/bin/cassandra_dump_schema b/hack/bin/cassandra_dump_schema index 01036fc0e61..4bad9cd87c0 100755 --- a/hack/bin/cassandra_dump_schema +++ b/hack/bin/cassandra_dump_schema @@ -9,7 +9,7 @@ import re def run_cqlsh(container, expr): p = ( subprocess.run( - ["docker", "exec", "-i", container, "/usr/bin/cqlsh", "-e", expr], + ["docker", "exec", "-i", container, "cqlsh", "-e", expr], stdout=PIPE, check=True, ) diff --git a/hack/bin/change_emails.py b/hack/bin/change_emails.py index 21b70cc2cb1..a587977d7d2 100755 --- a/hack/bin/change_emails.py +++ b/hack/bin/change_emails.py @@ -67,12 +67,12 @@ def put_scim_user(ctx, user_id, body): url = ctx.mkurl("spar", f"scim/v2/Users/{user_id}") return ctx.request('PUT', url, headers=({'Authorization': f'Bearer {scim_token}'}), json=body) -def get_activation_code(ctx, user_id, email): - url = ctx.mkurl("brig", f"i/users/activation-code", internal=True) +def get_activation_code(ctx, email): + url = ctx.mkurl("brig", "i/users/activation-code", internal=True) return ctx.request('GET', url, params=({'email': email})) def confirm_new_email(ctx, user_id, key, code): - url = ctx.mkurl("brig", f"/activate") + url = ctx.mkurl("brig", "/activate") return ctx.request('GET', url, headers=({'Z-User': user_id}), params=({'key': key, 'code': code})) def assert_resp(resp, status_want): @@ -89,7 +89,7 @@ def assert_resp(resp, status_want): ### idioms def confirm_email(user_id, email): - r = get_activation_code(ctx, user_id, email) + r = get_activation_code(ctx, email) assert_resp(r, 200) r2 = confirm_new_email(ctx, user_id, r.json()['key'], r.json()['code']) assert_resp(r2, 200) diff --git a/hack/bin/copy-charts.sh b/hack/bin/copy-charts.sh index e168e89d5a8..0c05bc19956 100755 --- a/hack/bin/copy-charts.sh +++ b/hack/bin/copy-charts.sh @@ -17,7 +17,7 @@ mkdir -p .local/charts rm -rf "${CHART_DEST:?}/$CHART" cp -r "$CHART_SOURCE/$CHART" "$CHART_DEST/" -if [ -f "$CHART_SOURCE/$CHART/requirements.yaml" ]; then +if [[ -f "$CHART_SOURCE/$CHART/requirements.yaml" ]]; then # very hacky bash, I'm sorry for subpath in $(grep "file://" "$CHART_SOURCE/$CHART/requirements.yaml" | awk '{ print $2 }' | xargs -n 1 | cut -c 8-) do diff --git a/hack/bin/create-docker-compose-sboms.sh b/hack/bin/create-docker-compose-sboms.sh new file mode 100755 index 00000000000..1b4c223938a --- /dev/null +++ b/hack/bin/create-docker-compose-sboms.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Find git repository root to ensure paths work regardless of where script is executed +GIT_ROOT="$(git rev-parse --show-toplevel)" + +# Source common SBOM functions +# shellcheck source=hack/bin/sbom-common.sh +source "$(dirname "$0")/sbom-common.sh" + +OUTPUT_DIR="${1:-.}" +COMPOSE_FILE_RELATIVE="deploy/dockerephemeral/docker-compose.yaml" +COMPOSE_FILE="$GIT_ROOT/$COMPOSE_FILE_RELATIVE" + +mkdir -p "$OUTPUT_DIR" + +# Get current git commit hash for linking to source +GIT_COMMIT=$(git rev-parse HEAD) +COMPOSE_FILE_URL="https://github.com/wireapp/wire-server/blob/${GIT_COMMIT}/${COMPOSE_FILE_RELATIVE}" + +# Track errors during processing +error_count=0 + +docker compose -f "$COMPOSE_FILE" config --images \ + | while read -r img; do + canonical_img=$(canonicalize_image_name "$img") + safe_name=$(echo "$canonical_img" | tr '/:' '-') + filename="$OUTPUT_DIR/sbom-${safe_name}.cyclonedx.json" + temp_filename="${filename}.tmp" + + echo " Creating SBOM for $img -> $canonical_img: $filename" + + if ! scan_image_with_syft "$canonical_img" "$temp_filename" "$OUTPUT_DIR"; then + ((error_count++)) + continue + fi + + purl=$(generate_oci_purl "$canonical_img") + + # Add fields that are not provided by syft + jq --arg purl "$purl" \ + --arg compose_url "$COMPOSE_FILE_URL" \ + '.metadata.component.name = $purl | + .metadata.component.purl = $purl | + .metadata.component.properties += [{"name": "scope", "value": "test"}] | + .metadata.component.externalReferences += [{"type": "build-meta", "url": $compose_url, "comment": "Source docker-compose manifest"}]' \ + "$temp_filename" > "$filename" + rm "$temp_filename" + done + +echo "SBOM generation complete. Output directory: $OUTPUT_DIR" + +if [[ $error_count -gt 0 ]]; then + echo "WARNING: $error_count error(s) occurred during SBOM generation" >&2 + exit 1 +fi diff --git a/hack/bin/create-helm-sboms.sh b/hack/bin/create-helm-sboms.sh new file mode 100755 index 00000000000..c113d78e038 --- /dev/null +++ b/hack/bin/create-helm-sboms.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Find git repository root to ensure paths work regardless of where script is executed +GIT_ROOT="$(git rev-parse --show-toplevel)" + +# Source common SBOM functions +# shellcheck source=hack/bin/sbom-common.sh +source "$(dirname "$0")/sbom-common.sh" + +OUTPUT_DIR="${1:-.}" +VERSION_OVERRIDE="${2:-}" +CHARTS_DIR="$GIT_ROOT/.local/charts" + +if [[ -z "$VERSION_OVERRIDE" ]]; then + echo "Usage: $0 " + echo " output-dir: Directory to write SBOM files" + echo " version: Version to use for packaged charts (e.g., 5.28.22)" + exit 1 +fi + +# Extract images from a Helm chart using helm template +# This properly resolves images from subcharts and dependencies +extract_images_from_chart() { + local chart_path="$1" + local chart_name="$2" + + # First, try to build dependencies if requirements.yaml or Chart.yaml has dependencies + if [[ -f "$chart_path/requirements.yaml" ]] || grep -q "^dependencies:" "$chart_path/Chart.yaml" 2>/dev/null; then + echo " Building dependencies for $chart_name..." >&2 + (cd "$chart_path" && helm dependency build > /dev/null 2>&1) || true + fi + + # Template the chart and extract image references + # We use a dummy release name and set a global placeholder to be more lenient + # (we don't want to check the Helm chart, only extract its images) + local output + output=$(helm template test-release "$chart_path" --set-string 'global.placeholder=placeholder' 2>/dev/null) || true + + # Extract image values from the output using yq (jq wrapper) + # Recursively find all .image fields in objects and output unique values + echo "$output" | yq -r '.. | objects | .image? // empty' - 2>/dev/null | sort -u || true + return 0 +} + +mkdir -p "$OUTPUT_DIR" + +# Get current git commit hash for linking to source +GIT_COMMIT=$(git rev-parse HEAD) + +# Track errors during processing +error_count=0 + +# Loop through each chart directory +for chart_dir in "$CHARTS_DIR"/*/; do + chart_name=$(basename "$chart_dir") + + # Skip if not a directory or doesn't have Chart.yaml + if [[ ! -f "$chart_dir/Chart.yaml" ]]; then + continue + fi + + # `mlsstats`'s image is not publically available + if [[ "$chart_name" == "mlsstats" ]]; then + echo "Skipping chart: $chart_name (excluded)" + continue + fi + + echo "Processing chart: $chart_name" + + # Get chart version from Chart.yaml + chart_version=$(yq -r '.version // "unknown"' "$chart_dir/Chart.yaml") + + # Extract images using helm template + images=$(extract_images_from_chart "$chart_dir" "$chart_name") + + # Process each unique image + while IFS= read -r img; do + # Skip empty lines + [[ -z "$img" ]] && continue + + canonical_img=$(canonicalize_image_name "$img") + safe_name=$(echo "$canonical_img" | tr '/:' '-') + filename="$OUTPUT_DIR/sbom-helm-${chart_name}-${safe_name}.cyclonedx.json" + temp_filename="${filename}.tmp" + + echo " Creating SBOM for $img -> $canonical_img: $filename" + + # Scan image with syft (handles schema 1 conversion and validation) + if ! scan_image_with_syft "$canonical_img" "$temp_filename" "$OUTPUT_DIR"; then + ((error_count++)) + continue + fi + + oci_purl=$(generate_oci_purl "$canonical_img") + + # Create helm purl + # Format: pkg:helm/name@version + helm_purl="pkg:helm/${chart_name}@${chart_version}" + + # Relative path for GitHub URL + chart_relative_path="charts/$chart_name" + chart_url="https://github.com/wireapp/wire-server/tree/${GIT_COMMIT}/${chart_relative_path}" + + # Add fields that are not provided by syft + jq --arg oci_purl "$oci_purl" \ + --arg helm_purl "$helm_purl" \ + --arg chart_url "$chart_url" \ + '.metadata.component.name = $oci_purl | + .metadata.component.purl = $oci_purl | + .metadata.component.externalReferences += [ + {"type": "distribution", "url": $helm_purl, "comment": "Helm chart"}, + {"type": "build-meta", "url": $chart_url, "comment": "Source Helm chart"} + ]' \ + "$temp_filename" > "$filename" + rm "$temp_filename" + done <<< "$images" +done + +echo "SBOM generation complete. Output directory: $OUTPUT_DIR" + +if [[ $error_count -gt 0 ]]; then + echo "WARNING: $error_count error(s) occurred during SBOM generation" >&2 + exit 1 +fi diff --git a/hack/bin/create-helmfile-sboms.sh b/hack/bin/create-helmfile-sboms.sh new file mode 100755 index 00000000000..53a4d51143c --- /dev/null +++ b/hack/bin/create-helmfile-sboms.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Find git repository root to ensure paths work regardless of where script is executed +GIT_ROOT="$(git rev-parse --show-toplevel)" + +# Source common SBOM functions +# shellcheck source=hack/bin/sbom-common.sh +source "$(dirname "$0")/sbom-common.sh" + +OUTPUT_DIR="${1:-.}" +VERSION_OVERRIDE="${2:-}" +HELMFILE_PATH="hack/helmfile.yaml.gotmpl" + +if [[ -z "$VERSION_OVERRIDE" ]]; then + echo "Usage: $0 " + echo " output-dir: Directory to write SBOM files" + echo " version: Version to use for wire-server images (e.g., 5.28.9)" + exit 1 +fi + +mkdir -p "$OUTPUT_DIR" + +# Get current git commit hash for linking to source +GIT_COMMIT=$(git rev-parse HEAD) + +helmfile_url="https://github.com/wireapp/wire-server/blob/${GIT_COMMIT}/${HELMFILE_PATH}" + +echo "Processing helmfile: $HELMFILE_PATH" + +# Extract all images from helmfile template output +# We use helmfile template to render all releases and extract images +cd "$GIT_ROOT" + +# Set dummy environment variables required by helmfile +export NAMESPACE_1="${NAMESPACE_1:-dummy-namespace-1}" +export NAMESPACE_2="${NAMESPACE_2:-dummy-namespace-2}" +export FEDERATION_DOMAIN_1="${FEDERATION_DOMAIN_1:-fed1.example.com}" +export FEDERATION_DOMAIN_2="${FEDERATION_DOMAIN_2:-fed2.example.com}" +export FEDERATION_DOMAIN_BASE_1="${FEDERATION_DOMAIN_BASE_1:-base1.example.com}" +export FEDERATION_DOMAIN_BASE_2="${FEDERATION_DOMAIN_BASE_2:-base2.example.com}" +export FEDERATION_CA_CERTIFICATE="${FEDERATION_CA_CERTIFICATE:-dummy-cert}" +export ENTERPRISE_IMAGE_PULL_SECRET="${ENTERPRISE_IMAGE_PULL_SECRET:-dummy-secret}" + +# Extract images from helmfile +images=$(helmfile -f "$HELMFILE_PATH" template 2>/dev/null | yq -r '.. | objects | .image? // empty' - 2>/dev/null | sort -u || true) + +if [[ -z "$images" ]]; then + echo "No images found in helmfile" >&2 + exit 1 +fi + +# Track errors during processing +error_count=0 + +# Process each unique image +while IFS= read -r img; do + # Skip empty lines + [[ -z "$img" ]] && continue + + canonical_img=$(canonicalize_image_name "$img") + safe_name=$(echo "$canonical_img" | tr '/:' '-') + filename="$OUTPUT_DIR/sbom-helmfile-${safe_name}.cyclonedx.json" + temp_filename="${filename}.tmp" + + echo " Creating SBOM for $img -> $canonical_img: $filename" + + # Scan image with syft (handles schema 1 conversion and validation) + if ! scan_image_with_syft "$canonical_img" "$temp_filename" "$OUTPUT_DIR"; then + ((error_count++)) + continue + fi + + # Generate OCI purl + oci_purl=$(generate_oci_purl "$canonical_img") + + # Create helmfile reference purl for context + helmfile_purl="pkg:helm/helmfile@${VERSION_OVERRIDE}" + + # Add fields that are not provided by syft + jq --arg oci_purl "$oci_purl" \ + --arg helmfile_purl "$helmfile_purl" \ + --arg helmfile_url "$helmfile_url" \ + '.metadata.component.name = $oci_purl | + .metadata.component.purl = $oci_purl | + .metadata.component.externalReferences += [ + {"type": "distribution", "url": $helmfile_purl, "comment": "Helmfile deployment"}, + {"type": "build-meta", "url": $helmfile_url, "comment": "Source Helmfile"} + ]' \ + "$temp_filename" > "$filename" + rm "$temp_filename" +done <<< "$images" + +echo "SBOM generation complete. Output directory: $OUTPUT_DIR" + +if [[ $error_count -gt 0 ]]; then + echo "WARNING: $error_count error(s) occurred during SBOM generation" >&2 + exit 1 +fi diff --git a/hack/bin/create_team.sh b/hack/bin/create_team.sh index aa672a85941..7ec49b03d98 100755 --- a/hack/bin/create_team.sh +++ b/hack/bin/create_team.sh @@ -56,7 +56,7 @@ while getopts ":o:e:p:v:t:c:h:" opt; do done shift $((OPTIND -1)) -if [ "$#" -ne 0 ]; then +if [[ "$#" -ne 0 ]]; then echo "$USAGE" 1>&2 exit 1 fi diff --git a/hack/bin/create_team_members.sh b/hack/bin/create_team_members.sh index 1988fbd62cb..9199eb862af 100755 --- a/hack/bin/create_team_members.sh +++ b/hack/bin/create_team_members.sh @@ -59,12 +59,12 @@ while getopts ":a:t:h:c:" opt; do done shift $((OPTIND -1)) -if [ "$#" -ne 0 ]; then +if [[ "$#" -ne 0 ]]; then echo "$USAGE" 1>&2 exit 1 fi -if [ ! -e "$CSV_FILE" ]; then +if [[ ! -e "$CSV_FILE" ]]; then echo -e "\n\n*** I need the name of an existing csv file.\n\n" echo "$USAGE" 1>&2 exit 1 diff --git a/hack/bin/create_team_request_code.sh b/hack/bin/create_team_request_code.sh index 6e6d85d1d14..1624acf36d3 100755 --- a/hack/bin/create_team_request_code.sh +++ b/hack/bin/create_team_request_code.sh @@ -35,7 +35,7 @@ while getopts ":e:h:" opt; do done shift $((OPTIND -1)) -if [ "$#" -ne 0 ]; then +if [[ "$#" -ne 0 ]]; then echo "$USAGE" 1>&2 exit 1 fi diff --git a/hack/bin/create_test_team_admins.sh b/hack/bin/create_test_team_admins.sh index 6fffc6ef279..a7919fec56f 100755 --- a/hack/bin/create_test_team_admins.sh +++ b/hack/bin/create_test_team_admins.sh @@ -44,7 +44,7 @@ while getopts ":n:h:c" opt; do done shift $((OPTIND -1)) -if [ "$#" -ne 0 ]; then +if [[ "$#" -ne 0 ]]; then echo "$USAGE" 1>&2 exit 1 fi @@ -65,7 +65,7 @@ do UUID=$(echo "$CURL_OUT" | tail -1 | sed 's/.*\"id\":\"\([a-z0-9-]*\)\".*/\1/') TEAM=$(echo "$CURL_OUT" | tail -1 | sed 's/.*\"team\":\"\([a-z0-9-]*\)\".*/\1/') - if [ "$CSV" == "false" ] + if [[ "$CSV" == "false" ]] then echo -e "Succesfully created a team admin user: $UUID on team: $TEAM with email: $EMAIL and password: $PASSWORD" else echo -e "$UUID,$EMAIL,$PASSWORD" fi diff --git a/hack/bin/create_test_team_members.sh b/hack/bin/create_test_team_members.sh index 417253b94f3..39dd9892305 100755 --- a/hack/bin/create_test_team_members.sh +++ b/hack/bin/create_test_team_members.sh @@ -66,14 +66,14 @@ while getopts ":a:t:s:n:h:d:c" opt; do done shift $((OPTIND -1)) -if [ "$#" -ne 0 ]; then +if [[ "$#" -ne 0 ]]; then echo "$USAGE" 1>&2 exit 1 fi # Warn about sending emails -if [ "$TARGET_EMAIL_DOMAIN" == "" ]; then +if [[ "$TARGET_EMAIL_DOMAIN" == "" ]]; then echo -e "\n\n*** Please provide an email domain if you want to run this script.\n\n" echo "$USAGE" 1>&2 exit 1 @@ -101,7 +101,7 @@ do if ( ( echo "$INVITATION_ID" | grep -q '"code"' ) && ( echo "$INVITATION_ID" | grep -q '"label"' ) ) ; then - echo "Got an error while creating $EMAIL, aborting: $INVITATION_ID" + echo "Got an error while creating $EMAIL, aborting: $INVITATION_ID" 1>&2 exit 1 fi @@ -123,13 +123,13 @@ do TEAM_MEMBER_UUID=$(echo "$CURL_OUT" | tail -1 | sed 's/.*\"id\":\"\([a-z0-9-]*\)\".*/\1/') TEAM=$(echo "$CURL_OUT" | tail -1 | sed 's/.*\"team\":\"\([a-z0-9-]*\)\".*/\1/') - if [ "$TEAM" != "$TEAM_UUID" ]; then - echo "unexpected error: user got assigned to no / the wrong team?!" + if [[ "$TEAM" != "$TEAM_UUID" ]]; then + echo "unexpected error: user got assigned to no / the wrong team?!" 1>&2 echo "${CURL_OUT}" exit 1 fi - if [ "$CSV" == "false" ] + if [[ "$CSV" == "false" ]] then echo -e "Succesfully created a team member: $TEAM_MEMBER_UUID on team: $TEAM_UUID with email: $EMAIL and password: $PASSWORD" else echo -e "$UUID,$EMAIL,$PASSWORD" fi diff --git a/hack/bin/create_test_team_scim.sh b/hack/bin/create_test_team_scim.sh index fcbb4498b53..f4cad645f02 100755 --- a/hack/bin/create_test_team_scim.sh +++ b/hack/bin/create_test_team_scim.sh @@ -39,7 +39,7 @@ while getopts ":h:s:" opt; do done shift $((OPTIND -1)) -if [ "$#" -ne 0 ]; then +if [[ "$#" -ne 0 ]]; then echo "$USAGE" 1>&2 exit 1 fi @@ -90,7 +90,7 @@ sleep 1 if ( ( echo "$INVITATION_ID" | grep -q '"code"' ) && ( echo "$INVITATION_ID" | grep -q '"label"' ) ) ; then - echo "Got an error while creating $REGULAR_USER_EMAIL, aborting: $INVITATION_ID" + echo "Got an error while creating $REGULAR_USER_EMAIL, aborting: $INVITATION_ID" 1>&2 exit 1 fi @@ -98,7 +98,7 @@ sleep 1 if ( ( echo "$INVITATION_ID" | grep -q '"code"' ) && ( echo "$INVITATION_ID" | grep -q '"label"' ) ) ; then - echo "Got an error while creating $REGULAR_USER_EMAIL, aborting: $INVITATION_ID" + echo "Got an error while creating $REGULAR_USER_EMAIL, aborting: $INVITATION_ID" 1>&2 exit 1 fi @@ -180,8 +180,8 @@ CURL_OUT=$(curl \ SCIM_USER_REGISTER_TEAM=$(echo "$CURL_OUT" | jq -r .team) -if [ "$SCIM_USER_REGISTER_TEAM" != "$TEAM_UUID" ]; then - echo "unexpected error: user got assigned to no / the wrong team?!" +if [[ "$SCIM_USER_REGISTER_TEAM" != "$TEAM_UUID" ]]; then + echo "unexpected error: user got assigned to no / the wrong team?!" 1>&2 echo "${CURL_OUT}" exit 1 fi diff --git a/hack/bin/create_test_user.sh b/hack/bin/create_test_user.sh index 430e5d51410..e803cdafb16 100755 --- a/hack/bin/create_test_user.sh +++ b/hack/bin/create_test_user.sh @@ -40,7 +40,7 @@ while getopts ":n:h:c" opt; do done shift $((OPTIND -1)) -if [ "$#" -ne 0 ]; then +if [[ "$#" -ne 0 ]]; then echo "$USAGE" 1>&2 exit 1 fi; @@ -60,7 +60,7 @@ do UUID=$(echo "$CURL_OUT" | tail -1 | sed 's/.*\"id\":\"\([a-z0-9-]*\)\".*/\1/') - if [ "$CSV" == "false" ] + if [[ "$CSV" == "false" ]] then echo -e "Succesfully created a user with email: ""$EMAIL"" and password: ""$PASSWORD" else echo -e "$UUID,$EMAIL,$PASSWORD" fi diff --git a/hack/bin/diff-wire-server-manifests.sh b/hack/bin/diff-wire-server-manifests.sh new file mode 100755 index 00000000000..124ff059740 --- /dev/null +++ b/hack/bin/diff-wire-server-manifests.sh @@ -0,0 +1,184 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: + ./hack/bin/diff-wire-server-manifests.sh BEFORE_MANIFEST AFTER_MANIFEST [OUTPUT_DIR] + +Examples: + ./hack/bin/diff-wire-server-manifests.sh /tmp/before.yaml /tmp/after.yaml + ./hack/bin/diff-wire-server-manifests.sh /tmp/before.yaml /tmp/after.yaml /tmp/wire-server-diff + +Splits each manifest into one file per YAML document and compares the resulting +directories with `git diff --no-index`. + +Generated file names are based on: + --.yaml + +If multiple documents resolve to the same base name, a numeric suffix is added. + +If OUTPUT_DIR is omitted, a temporary directory is used. + +Optional environment variables: + DIFF_OUTPUT_FILE=/tmp/wire-server-manifest.diff +EOF +} + +if [[ $# -lt 2 || $# -gt 3 ]]; then + usage + exit 2 +fi + +before_manifest=$1 +after_manifest=$2 +output_dir=${3:-} +diff_output_file=${DIFF_OUTPUT_FILE:-} + +if [[ ! -f "$before_manifest" ]]; then + echo "Missing before manifest: $before_manifest" >&2 + exit 2 +fi + +if [[ ! -f "$after_manifest" ]]; then + echo "Missing after manifest: $after_manifest" >&2 + exit 2 +fi + +if ! command -v yq >/dev/null 2>&1; then + echo "Missing dependency: yq" >&2 + exit 2 +fi + +if ! command -v git >/dev/null 2>&1; then + echo "Missing dependency: git" >&2 + exit 2 +fi + +if [[ -z "$output_dir" ]]; then + output_dir=$(mktemp -d) + cleanup_output_dir=true +else + mkdir -p "$output_dir" + cleanup_output_dir=false +fi + +# shellcheck disable=SC2329 +cleanup() { + if [[ "${cleanup_output_dir}" == "true" ]]; then + rm -rf "$output_dir" + fi +} +trap cleanup EXIT + +before_dir="$output_dir/before" +after_dir="$output_dir/after" + +rm -rf "$before_dir" "$after_dir" +mkdir -p "$before_dir" "$after_dir" + +sanitize_filename_part() { + local value=$1 + value=${value// /_} + value=${value//\//_} + value=${value//:/_} + value=${value//[^[:alnum:]._-]/-} + printf '%s\n' "$value" +} + +render_manifest_dir() { + local manifest=$1 + local target_dir=$2 + local count=0 + + declare -A seen=() + + while IFS= read -r -d '' doc; do + [[ -z "$doc" ]] && continue + + local kind + local name + local namespace + local base_name + local suffix + local file_name + + kind=$(yq -r '.kind // ""' <<<"$doc" 2>/dev/null || true) + name=$(yq -r '.metadata.name // ""' <<<"$doc" 2>/dev/null || true) + namespace=$(yq -r '.metadata.namespace // "default"' <<<"$doc" 2>/dev/null || true) + + # Skip empty/comment-only or otherwise unparsable YAML documents. + if [[ -z "$kind" || -z "$name" ]]; then + continue + fi + + base_name="$(sanitize_filename_part "$namespace")-$(sanitize_filename_part "$kind")-$(sanitize_filename_part "$name")" + suffix=${seen["$base_name"]:-0} + + if [[ "$suffix" -eq 0 ]]; then + file_name="$base_name.yaml" + else + file_name="$base_name-$suffix.yaml" + fi + seen["$base_name"]=$((suffix + 1)) + + printf '%s' "$doc" >"$target_dir/$file_name" + count=$((count + 1)) + done < <( + awk ' + BEGIN { doc = "" } + /^---[[:space:]]*$/ { + if (doc != "") { + printf "%s%c", doc, 0 + doc = "" + } + next + } + { doc = doc $0 ORS } + END { + if (doc != "") { + printf "%s%c", doc, 0 + } + } + ' "$manifest" + ) + + echo "$count" +} + +before_count=$(render_manifest_dir "$before_manifest" "$before_dir") +after_count=$(render_manifest_dir "$after_manifest" "$after_dir") + +echo "Before manifest resources: $before_count" +echo "After manifest resources: $after_count" +echo "Before directory: $before_dir" +echo "After directory: $after_dir" +if [[ -n "$diff_output_file" ]]; then + echo "Diff output file: $diff_output_file" +fi +echo + +set +e +if [[ -n "$diff_output_file" ]]; then + mkdir -p "$(dirname "$diff_output_file")" + git diff --no-index -- "$before_dir" "$after_dir" >"$diff_output_file" +else + git diff --no-index -- "$before_dir" "$after_dir" +fi +diff_exit=$? +set -e + +if [[ $diff_exit -eq 0 ]]; then + echo "No differences found." + exit 0 +fi + +if [[ $diff_exit -eq 1 ]]; then + echo + echo "Differences found." + exit 1 +fi + +echo "git diff failed with exit code $diff_exit" >&2 +exit "$diff_exit" diff --git a/hack/bin/find-latest-docker-tag.sh b/hack/bin/find-latest-docker-tag.sh index cca6ca5d642..127493ea6a3 100755 --- a/hack/bin/find-latest-docker-tag.sh +++ b/hack/bin/find-latest-docker-tag.sh @@ -11,6 +11,7 @@ function lookup() { curl -sSL "https://quay.io/api/v1/repository/wire/$image/tag/?limit=50&page=1&onlyActiveTags=true" \ | jq -r '.tags[].name' \ | sort --version-sort | uniq | grep -v latest | grep -v 'pr\.' | tail -1 + return 0 } lookup brig diff --git a/hack/bin/gen-certs.sh b/hack/bin/gen-certs.sh index 462f321e1ba..f995a238aaa 100755 --- a/hack/bin/gen-certs.sh +++ b/hack/bin/gen-certs.sh @@ -18,9 +18,11 @@ trap cleanup EXIT # Generate self-signed CA certificate and key at root/ca.pem and # root/ca-key.pem respectively. gen_ca() { - echo "generating CA: $2" - openssl req -x509 -newkey rsa:2048 -keyout "$1/ca-key.pem" -out "$1/ca.pem" -sha256 -days 3650 -nodes -subj "/CN=$2" 2>/dev/null - + local root="$1" + local name="$2" + echo "generating CA: $name" + openssl req -x509 -newkey rsa:2048 -keyout "$root/ca-key.pem" -out "$root/ca.pem" -sha256 -days 3650 -nodes -subj "/CN=$name" 2>/dev/null + return 0 } # usage: gen_cert root san name @@ -29,12 +31,16 @@ gen_ca() { # and ca-key.pem exist in the same directory. The generated certificate and # private key will end up in root/cert.pem and root/key.pem. gen_cert() { - echo "generating certificate: $2" + local root="$1" + local san="$2" + local name="$3" + echo "generating certificate: $name" subj=() - if [ -n "$3" ]; then - subj=(-subj "/CN=$3") + if [[ -n "$name" ]]; then + subj=(-subj "/CN=$name") fi - openssl x509 -req -in <(openssl req -nodes -newkey rsa:2048 -keyout "$1/key.pem" -out /dev/stdout -subj "/" 2>/dev/null) -CA "$1/ca.pem" -CAkey "$1/ca-key.pem" "${subj[@]}" -out "$1/cert.pem" -set_serial 0 -days 3650 -extfile <( echo "extendedKeyUsage = serverAuth, clientAuth"; echo "subjectAltName = critical, $2" ) 2>/dev/null + openssl x509 -req -in <(openssl req -nodes -newkey rsa:2048 -keyout "$root/key.pem" -out /dev/stdout -subj "/" 2>/dev/null) -CA "$root/ca.pem" -CAkey "$root/ca-key.pem" "${subj[@]}" -out "$root/cert.pem" -set_serial 0 -days 3650 -extfile <( echo "extendedKeyUsage = serverAuth, clientAuth"; echo "subjectAltName = critical, $san" ) 2>/dev/null + return 0 } # usage: install_certs source_dir target_dir ca ca-key cert key @@ -42,10 +48,17 @@ gen_cert() { # Copy certificates into the target directory, using the given file names. If a # name is empty, the corresponding certificate is skipped. install_certs() { - if [ -n "$3" ]; then cp "$1/ca.pem" "$2/$3.pem"; fi - if [ -n "$4" ]; then cp "$1/ca-key.pem" "$2/$4.pem"; fi - if [ -n "$5" ]; then cp "$1/cert.pem" "$2/$5.pem"; fi - if [ -n "$6" ]; then cp "$1/key.pem" "$2/$6.pem"; fi + local source_dir="$1" + local target_dir="$2" + local ca="$3" + local ca_key="$4" + local cert="$5" + local key="$6" + if [[ -n "$ca" ]]; then cp "$source_dir/ca.pem" "$target_dir/$ca.pem"; fi + if [[ -n "$ca_key" ]]; then cp "$source_dir/ca-key.pem" "$target_dir/$ca_key.pem"; fi + if [[ -n "$cert" ]]; then cp "$source_dir/cert.pem" "$target_dir/$cert.pem"; fi + if [[ -n "$key" ]]; then cp "$source_dir/key.pem" "$target_dir/$key.pem"; fi + return 0 } # federation diff --git a/hack/bin/helm-render-ci-values.sh b/hack/bin/helm-render-ci-values.sh new file mode 100755 index 00000000000..a1f3a6babc2 --- /dev/null +++ b/hack/bin/helm-render-ci-values.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# best run via make render-wire-server-resources +# otherwise run make clean-charts and make charts-integration before + +set -euo pipefail + +VALUES_FILE="${VALUES_FILE:-$(mktemp).yaml}" + +# from repo root +export NAMESPACE_1="${NAMESPACE_1:-test-a}" +export NAMESPACE_2="${NAMESPACE_2:-test-b}" +export FEDERATION_DOMAIN_1="${FEDERATION_DOMAIN_1:-integration.example.com}" +export FEDERATION_DOMAIN_2="${FEDERATION_DOMAIN_2:-integration2.example.com}" +export FEDERATION_DOMAIN_BASE_1="${FEDERATION_DOMAIN_BASE_1:-example.com}" +export FEDERATION_DOMAIN_BASE_2="${FEDERATION_DOMAIN_BASE_2:-example.com}" +export FEDERATION_CA_CERTIFICATE="${FEDERATION_CA_CERTIFICATE:-$(cat services/nginz/integration-test/conf/nginz/integration-ca.pem)}" +export ENTERPRISE_IMAGE_PULL_SECRET="${ENTERPRISE_IMAGE_PULL_SECRET:-{}}" + +helmfile -f hack/helmfile.yaml.gotmpl \ + -e default \ + --skip-deps \ + -l name=wire-server,namespace="${NAMESPACE_1}" \ + write-values \ + --output-file-template "${VALUES_FILE}" + +echo "Rendered values: $VALUES_FILE" diff --git a/hack/bin/helm-template.sh b/hack/bin/helm-template.sh index 03e88d5abe9..2d48dced048 100755 --- a/hack/bin/helm-template.sh +++ b/hack/bin/helm-template.sh @@ -17,12 +17,12 @@ CHARTS_DIR="${TOP_LEVEL}/.local/charts" valuesfile="${DIR}/../helm_vars/${chart}/values.yaml" certificatesfile="${DIR}/../helm_vars/${chart}/certificates.yaml" declare -a options=() -if [ -f "$valuesfile" ]; then +if [[ -f "$valuesfile" ]]; then options+=(-f "$valuesfile") fi -if [ -f "$certificatesfile" ]; then +if [[ -f "$certificatesfile" ]]; then options+=(-f "$certificatesfile") fi "$DIR/update.sh" "$CHARTS_DIR/$chart" -helm template "$chart" "$CHARTS_DIR/$chart" "${options[*]}" +helm template "$chart" "$CHARTS_DIR/$chart" "${options[@]}" diff --git a/hack/bin/integration-cleanup.sh b/hack/bin/integration-cleanup.sh index de5d24a1471..e17e8721ba0 100755 --- a/hack/bin/integration-cleanup.sh +++ b/hack/bin/integration-cleanup.sh @@ -21,7 +21,7 @@ namespaces=$(kubectl get namespaces -o json | jq -r --argjson now "$NOW" ' | $name ') -if [ -z "$namespaces" ]; then +if [[ -z "$namespaces" ]]; then echo "Nothing to clean up." else while read -r namespace; do diff --git a/hack/bin/integration-logs-relevant-bits.sh b/hack/bin/integration-logs-relevant-bits.sh index 96b836ca906..19cb822ab6c 100755 --- a/hack/bin/integration-logs-relevant-bits.sh +++ b/hack/bin/integration-logs-relevant-bits.sh @@ -32,6 +32,7 @@ excludeLogEntries() { grep -v '^{".*Info"' | grep -v '^{".*Debug"' | grep -v '^20.*, D, .*socket: [0-9]\+>$' + return 0 } cleanup() { @@ -43,12 +44,14 @@ cleanup() { sed '/^Progress [0-9]\+/d' | sed '/^\s\+$/d' | sed 's/:\s\+{/:\n{/g' + return 0 } grepper() { # print 10 lines before/after for context rg "$problem_markers" --color=always -A 10 -B 10 echo -e "\033[0m" + return 0 } cleanup | excludeLogEntries | grepper diff --git a/hack/bin/integration-teardown-ingress-classes.sh b/hack/bin/integration-teardown-ingress-classes.sh index 8fa66de06f8..5dec156f0ec 100755 --- a/hack/bin/integration-teardown-ingress-classes.sh +++ b/hack/bin/integration-teardown-ingress-classes.sh @@ -20,7 +20,7 @@ kubectl get ingressclasses -o json | jq -r --argjson now "$now" ' | if ($now - $created > 86400) then $meta.name else empty end ' > "$tmpfile" -if [ ! -s "$tmpfile" ]; then +if [[ ! -s "$tmpfile" ]]; then echo "No IngressClasses older than 24 hours found for deletion." rm "$tmpfile" exit 0 diff --git a/hack/bin/integration-test.sh b/hack/bin/integration-test.sh index c53003b1626..198750ee081 100755 --- a/hack/bin/integration-test.sh +++ b/hack/bin/integration-test.sh @@ -20,11 +20,13 @@ cleanup() { rm -f "logs-$t" done fi + return 0 } # Copy to the concourse output (indetified by $OUTPUT_DIR) for propagation to # following steps. copyToAwsS3() { + local build_ts build_ts=$(date +%s) if ((UPLOAD_LOGS > 0)); then for t in "${tests[@]}"; do @@ -32,6 +34,7 @@ copyToAwsS3() { aws s3 cp "logs-$t" "s3://wire-server-test-logs/test-logs-$VERSION/$t-$VERSION-$build_ts.log" done fi + return 0 } summary() { @@ -48,6 +51,7 @@ summary() { echo "$t-integration passed ✅." fi done + return 0 } # Copy the secrets from the wire-federation-v0 namespace to the current namespace to be able to delete RabbitMQ queues that are created by the integration tests to avoid overflows diff --git a/hack/bin/oauth_test.sh b/hack/bin/oauth_test.sh index 1a6caad9506..a8160c11323 100755 --- a/hack/bin/oauth_test.sh +++ b/hack/bin/oauth_test.sh @@ -30,7 +30,7 @@ while getopts ":u:" opt; do done shift $((OPTIND - 1)) -if [ -z "$USER" ]; then +if [[ -z "$USER" ]]; then echo 'missing option -u ' 1>&2 echo "$USAGE" 1>&2 exit 1 diff --git a/hack/bin/performance.py b/hack/bin/performance.py index bd39b835a3f..d8845a7243e 100755 --- a/hack/bin/performance.py +++ b/hack/bin/performance.py @@ -54,6 +54,9 @@ LAST_PREKEY = "pQABARn//wKhAFggnCcZIK1pbtlJf4wRQ44h4w7/sfSgj5oWXMQaUGYAJ/sDoQChAFgglacihnqg/YQJHkuHNFU7QD6Pb3KN4FnubaCF2EVOgRkE9g==" +RES_CREATION_JSON = "res_creation.json" + +RES_REGISTER_JSON = "res_register.json" def save_json_file(ob, path): with open(path, "w") as f: @@ -61,7 +64,11 @@ def save_json_file(ob, path): def load_json_file(path): - with open(path, "r") as f: + # Validate path to prevent path injection attacks + normalized_path = os.path.normpath(path) + if '..' in normalized_path or normalized_path.startswith('/') or ':' in normalized_path: + raise ValueError(f"Invalid path: {path}") + with open(normalized_path, "r") as f: return json.load(f) @@ -142,10 +149,7 @@ def __init__(self): self.users_zuid_cookie = {} self.last_switched_user = None - def mkurl(self, service, relative_url, internal=False): - # if not internal: - # service = "nginz" - + def mkurl(self, service, relative_url): name = "WIREAPI_BASEURL_" + service.upper() baseurl = os.environ[name] if (not relative_url.startswith("/access")) and (not relative_url.startswith('/register')) and (not relative_url.startswith('/i')) and (not relative_url.startswith("/login")): @@ -186,7 +190,6 @@ def request(self, method, url, user=None, conn_id="conn", client=None, headers=N if res.status_code == 401: res_refresh = api.create_access_token(self) if res_refresh.status_code != 200: - msg = "Refreshing the access token failed: \n" msg = pretty_response(res_refresh) raise ValueError(msg) else: @@ -332,7 +335,7 @@ def create_admin(ctx, basedir): os.makedirs(ud_temp, exist_ok=True) res_creation = save( - api.create_user(ctx, create_team=True), j(ud_temp, "res_creation.json") + api.create_user(ctx, create_team=True), j(ud_temp, RES_CREATION_JSON) ) simple_expect_status(201, res_creation) @@ -340,8 +343,8 @@ def create_admin(ctx, basedir): ud = user_dir(basedir, user_id) os.makedirs(ud, exist_ok=True) - dest = j(ud, "res_creation.json") - shutil.move(j(ud_temp, "res_creation.json"), dest) + dest = j(ud, RES_CREATION_JSON) + shutil.move(j(ud_temp, RES_CREATION_JSON), dest) print(f"Moving to {dest}") res_login = save( @@ -407,7 +410,7 @@ def admin_user_dir(basedir): def create_user(ctx, basedir): admin_dir = admin_user_dir(basedir) - admin = load_json_file(j(admin_dir, "res_creation.json")) + admin = load_json_file(j(admin_dir, RES_CREATION_JSON)) admin_user = admin["response"]["content"]["id"] team = admin["response"]["content"]["team"] @@ -431,7 +434,7 @@ def create_user(ctx, basedir): res_register = save( api.register_user(ctx, email=email_invitee, code=code), - j(ud_temp, "res_register.json"), + j(ud_temp, RES_REGISTER_JSON), ) simple_expect_status(201, res_register) assert res_register["response"]["content"]["team"] == team @@ -440,8 +443,8 @@ def create_user(ctx, basedir): ud = user_dir(basedir, user_id) os.makedirs(ud, exist_ok=True) - dest = j(ud, "res_register.json") - shutil.move(j(ud_temp, "res_register.json"), dest) + dest = j(ud, RES_REGISTER_JSON) + shutil.move(j(ud_temp, RES_REGISTER_JSON), dest) print("Moving to", dest) res_login = save(api.login(ctx, email_invitee), j(ud, "res_login.json")) @@ -461,11 +464,11 @@ def create_mls_client(ctx, basedir, user_id): # is there an easier way to get the domain of the user? ud = user_dir(basedir, user_id) - res_creation_file = j(ud, "res_creation.json") + res_creation_file = j(ud, RES_CREATION_JSON) if os.path.exists(res_creation_file): user_file = res_creation_file else: - user_file = j(ud, "res_register.json") + user_file = j(ud, RES_REGISTER_JSON) res_user = load_json_file(user_file) quid = res_user["response"]["content"]["qualified_id"] domain = quid["domain"] @@ -519,7 +522,7 @@ def list_clients(user_dir): return os.listdir(j(user_dir, "clients")) -def defNewConvMLS(client_id): +def def_new_conv_mls(client_id): return { "name": "conv default name", "access": [], @@ -547,12 +550,11 @@ def create_mls_conv(ctx, basedir): Assumes admin is logged in """ ud = admin_user_dir(basedir) - admin = load_json_file(j(ud, "res_creation.json"))['response']['content'] # create conv client_id = list_clients(ud)[0] - admin = load_json_file(j(ud, "res_creation.json"))['response']['content'] + admin = load_json_file(j(ud, RES_CREATION_JSON))['response']['content'] cdir = client_dir(ud, client_id) @@ -564,7 +566,7 @@ def create_mls_conv(ctx, basedir): conv_id = res_post_conv['response']['content']['qualified_id']['id'] else: res_post_conv = save( - api.create_conversation(ctx, user=admin, **defNewConvMLS(client_id)), + api.create_conversation(ctx, user=admin, **def_new_conv_mls(client_id)), j(basedir, "res_post_conv.json"), ) simple_expect_status(201, res_post_conv) @@ -649,7 +651,7 @@ def create_client(i): cid = create_mls_client(ctx, basedir, user_id) ud = user_dir(basedir, user_id) - res_register = load_json_file(j(ud, "res_register.json")) + res_register = load_json_file(j(ud, RES_REGISTER_JSON)) quid = res_register["response"]["content"]["qualified_id"] res_kp_claim = save( api.claim_key_packages(ctx, user=quid, target=quid), @@ -686,7 +688,7 @@ def main_setup_add_participants(basedir, batchsize=500): admin_dir = user_dir(basedir, admin_id) - res_creation = load_json_file(j(admin_dir, "res_creation.json")) + res_creation = load_json_file(j(admin_dir, RES_CREATION_JSON)) admin = res_creation['response']['content'] res_login = save( api.login(ctx_admin, res_creation["request"]["body"]["email"]), @@ -694,9 +696,6 @@ def main_setup_add_participants(basedir, batchsize=500): ) simple_expect_status(200, res_login) - # admin_res_login = load_json_file(j(admin_dir, "res_login.json")) - # ctx_admin.load_cookies_from_response(admin_res_login) - client_id = list_clients(admin_dir)[0] upgrade_access_token(ctx_admin, basedir, user_id=admin_id, client_id=client_id) @@ -778,8 +777,6 @@ def main_setup_add_participants(basedir, batchsize=500): try: - # if i > 0: - # raise ValueError("intentional") simple_expect_status(201, res_post_commit_bundle) except: print("Restoring admin's client state") diff --git a/hack/bin/prepare-local-charts.sh b/hack/bin/prepare-local-charts.sh new file mode 100755 index 00000000000..8bc7dd34549 --- /dev/null +++ b/hack/bin/prepare-local-charts.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Prepare local helm charts by clearing repositories and updating/packaging charts +# Usage: prepare-local-charts.sh [chart1 chart2 ...] +# If no arguments provided, processes all charts in .local/charts + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +CHARTS_DIR="$SCRIPT_DIR/../../.local/charts" + +# get rid of all helm repositories +# We need to deal with helm repo list failing because of https://github.com/helm/helm/issues/10028 +(helm repo list -o json || echo '[]') | jq -r '.[] | .name' | xargs -I% helm repo remove % + +cd "$CHARTS_DIR" + +# If arguments provided, process those charts; otherwise process all subdirectories +if [[ $# -gt 0 ]]; then + charts=("$@") +else + # Find all chart directories (those containing Chart.yaml) + charts=() + for dir in */; do + if [[ -f "$dir/Chart.yaml" ]]; then + charts+=("${dir%/}") + fi + done +fi + +for chart in "${charts[@]}"; do + ../../hack/bin/update.sh "$chart" + helm package "$chart" +done diff --git a/hack/bin/register_idp.sh b/hack/bin/register_idp.sh index d2e0930ca3a..74e1568ac0e 100755 --- a/hack/bin/register_idp.sh +++ b/hack/bin/register_idp.sh @@ -16,18 +16,18 @@ set -e # metadata_file=$1 -if [ ! -e "$metadata_file" ]; then +if [[ ! -e "$metadata_file" ]]; then echo "*** no metadata: '$1'" exit 80 fi -if [ -n "$WIRE_BACKEND" ]; then +if [[ -n "$WIRE_BACKEND" ]]; then backend="$WIRE_BACKEND" else backend="localhost:8080" fi -if [ "$WIRE_TRACE" == "1" ]; then +if [[ "$WIRE_TRACE" == "1" ]]; then trace="1" fi @@ -38,14 +38,14 @@ command -v jq >/dev/null || ( echo "*** please install https://stedolan.github.i jq_exe=$(command -v jq) # login -if [ -n "$WIRE_LOGIN" ]; then +if [[ -n "$WIRE_LOGIN" ]]; then login="$WIRE_LOGIN" else echo -n "login email: " read -r login fi -if [ -n "$WIRE_PASSWORD" ]; then +if [[ -n "$WIRE_PASSWORD" ]]; then password="$WIRE_PASSWORD" else echo -n "password: " diff --git a/hack/bin/register_idp_internal.sh b/hack/bin/register_idp_internal.sh index c5bd491ac93..5608f414c10 100755 --- a/hack/bin/register_idp_internal.sh +++ b/hack/bin/register_idp_internal.sh @@ -8,13 +8,13 @@ set -e backend="http://localhost:8080" metadata_file=$1 -if [ ! -e "${metadata_file}" ]; then +if [[ ! -e "${metadata_file}" ]]; then echo "*** no metadata: '$1'" exit 80 fi z_user=$2 -if [ -z "${z_user}" ]; then +if [[ -z "${z_user}" ]]; then echo "*** no z_user uuid" exit 80 fi diff --git a/hack/bin/render-manifest.sh b/hack/bin/render-manifest.sh new file mode 100755 index 00000000000..9ca5932d9a1 --- /dev/null +++ b/hack/bin/render-manifest.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# best run via make render-manifest +# otherwise run make clean-charts and make charts-integration before + +set -euo pipefail + +if [[ $# -ne 1 ]]; then + cat >&2 <<'EOF' +Usage: render-manifest.sh + +Optional environment variables: + OUTPUT_FILE=/tmp/rendered.yaml +EOF + exit 1 +fi + +VALUES_FILE="$1" +OUTPUT_FILE="${OUTPUT_FILE:-/tmp/wire-server.yaml}" + +if [[ ! -f "$VALUES_FILE" ]]; then + echo "Values file not found: $VALUES_FILE" >&2 + exit 1 +fi + +helm_opts=( + --namespace wire + --no-hooks + -f "$VALUES_FILE" +) + +helm dependency build --skip-refresh ./.local/charts/wire-server +helm template wire-server ./.local/charts/wire-server \ + "${helm_opts[@]}" \ + > "$OUTPUT_FILE" + +echo "Rendered manifest: $OUTPUT_FILE" diff --git a/hack/bin/run-syft.sh b/hack/bin/run-syft.sh new file mode 100755 index 00000000000..46f1588cb2e --- /dev/null +++ b/hack/bin/run-syft.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Centralized syft invocation with all standard Wire server settings +# This ensures consistent syft configuration across all SBOM generation + +SOURCE="${1:-}" +OUTPUT_FILE="${2:-}" + +if [[ -z "$SOURCE" ]] || [[ -z "$OUTPUT_FILE" ]]; then + echo "Usage: $0 " >&2 + echo " source: syft source (e.g., 'docker:image:tag', 'oci-dir:/path/to/oci')" >&2 + echo " output-file: path to write CycloneDX JSON output" >&2 + exit 1 +fi + +# Standard syft configuration for Wire server SBOMs +SYFT_FORMAT_PRETTY=true \ +SYFT_ENRICH=all \ +SYFT_NIX_CAPTURE_OWNED_FILES=true \ +SYFT_SCOPE="all-layers" \ + syft -v "$SOURCE" -o cyclonedx-json > "$OUTPUT_FILE" diff --git a/hack/bin/sbom-common.sh b/hack/bin/sbom-common.sh new file mode 100755 index 00000000000..df0a1becc62 --- /dev/null +++ b/hack/bin/sbom-common.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash + +# Common functions and utilities for SBOM generation scripts + +# Canonicalize image names to include explicit docker.io or existing registry +# prefix. Docker's default registry is docker.io, but it's often omitted in +# image references. +canonicalize_image_name() { + local img="$1" + + if [[ "$img" != *"/"* ]]; then + # No slash means it's an official Docker Hub image (e.g., nginx:latest) + echo "docker.io/library/$img" + elif [[ "${img%%/*}" != *"."* ]] && [[ "${img%%/*}" != *":"* ]] && [[ "${img%%/*}" != "localhost" ]]; then + # Has slash but first part has no dot/colon and isn't localhost + # This is a Docker Hub user/org image (e.g., mesosphere/aws-cli:1.14.5) + echo "docker.io/$img" + else + # Already has a registry (e.g., quay.io/wire/brig:latest) + echo "$img" + fi + return 0 +} + +# Generate OCI purl from canonical image name +# Args: canonical_img +# Output: OCI purl string +generate_oci_purl() { + local canonical_img="$1" + + local registry="${canonical_img%%/*}" # Extract registry (e.g., quay.io) + local image_path="${canonical_img#*/}" # Remove registry (e.g., wire/brig:5.28.15) + local image_name="${image_path%:*}" # Remove tag (e.g., wire/brig) + local image_tag="${image_path##*:}" # Extract tag (e.g., 5.28.15) + + echo "pkg:oci/${image_name}@${image_tag}?repository_url=${registry}" + return 0 +} + +# Scan image with syft, handling schema 1 manifests if needed +# Args: canonical_img, temp_filename, output_dir +# Returns: 0 on success, 1 on failure +scan_image_with_syft() { + local canonical_img="$1" + local temp_filename="$2" + local output_dir="$3" + + # Locate run-syft.sh in the same directory as this script + local run_syft + run_syft="$(dirname "${BASH_SOURCE[0]}")/run-syft.sh" + + # Set up registry authentication if credentials are available + # This prevents "too many requests" issues when querying dockerhub images + # and allows access to private repositories on quay.io. + local syft_env=() + if [[ "$canonical_img" == docker.io/* ]] && [[ -n "${DOCKER_HUB_USERNAME:-}" ]] && [[ -n "${DOCKER_HUB_PASSWORD:-}" ]]; then + syft_env=( + "SYFT_REGISTRY_AUTH_USERNAME=$DOCKER_HUB_USERNAME" + "SYFT_REGISTRY_AUTH_PASSWORD=$DOCKER_HUB_PASSWORD" + ) + elif [[ "$canonical_img" == quay.io/* ]] && [[ -n "${QUAY_REPO_USER:-}" ]] && [[ -n "${QUAY_REPO_PASSWORD:-}" ]]; then + syft_env=( + "SYFT_REGISTRY_AUTH_USERNAME=$QUAY_REPO_USER" + "SYFT_REGISTRY_AUTH_PASSWORD=$QUAY_REPO_PASSWORD" + ) + fi + + # Check manifest version with skopeo to determine if conversion is needed + local manifest_info + manifest_info=$(skopeo inspect --raw "docker://$canonical_img" 2>/dev/null || echo "") + + if echo "$manifest_info" | grep -q '"schemaVersion":\s*1'; then + # Old schema 1 format - need to convert with skopeo + echo " Detected schema 1 manifest, converting to OCI format with skopeo..." >&2 + + local oci_dir + oci_dir="$output_dir/.oci-cache/$(echo "$canonical_img" | tr '/:' '-')" + mkdir -p "$oci_dir" + + if ! skopeo copy --insecure-policy "docker://$canonical_img" "oci:$oci_dir"; then + echo " ERROR: Failed to convert image $canonical_img with skopeo" >&2 + rm -rf "$oci_dir" + return 1 + fi + + # Scan the OCI format image + if ! env "${syft_env[@]}" "$run_syft" "oci-dir:$oci_dir" "$temp_filename"; then + echo " ERROR: Failed to scan OCI image for $canonical_img" >&2 + rm -rf "$oci_dir" + rm -f "$temp_filename" + return 1 + fi + else + # Modern format - scan directly with syft + if ! env "${syft_env[@]}" "$run_syft" "registry:$canonical_img" "$temp_filename"; then + echo " ERROR: Failed to generate SBOM for $canonical_img" >&2 + rm -f "$temp_filename" + return 1 + fi + fi + + # Validate the generated SBOM + _validate_sbom_json "$temp_filename" "$canonical_img" +} + +# Validate SBOM JSON file (internal helper) +# Args: temp_filename, canonical_img +# Returns: 0 if valid, 1 if invalid +_validate_sbom_json() { + local temp_filename="$1" + local canonical_img="$2" + + if [[ ! -s "$temp_filename" ]]; then + echo " ERROR: Empty SBOM output for $canonical_img" >&2 + rm -f "$temp_filename" + return 1 + fi + + if ! jq empty "$temp_filename" 2>/dev/null; then + echo " ERROR: Invalid JSON in SBOM output for $canonical_img:" >&2 + head -5 "$temp_filename" >&2 + rm -f "$temp_filename" + return 1 + fi + + return 0 +} diff --git a/hack/bin/set-helm-chart-version.sh b/hack/bin/set-helm-chart-version.sh index 4a96a7ae9aa..801f08bdfdc 100755 --- a/hack/bin/set-helm-chart-version.sh +++ b/hack/bin/set-helm-chart-version.sh @@ -11,25 +11,27 @@ tempfile=$(mktemp) # (sed usage should be portable for both GNU sed and BSD (Mac OS) sed) function update_chart(){ - chart_file=$1 + local chart_file="$1" sed -e "s/^version: .*/version: $target_version/g" "$chart_file" > "$tempfile" && mv "$tempfile" "$chart_file" + return 0 } function write_versions() { - target_version=$1 + local target_version="$1" # update chart version update_chart Chart.yaml # update all dependencies, if any - if [ -e 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 + if [[ -d "$CHARTS_DIR/$dep" ]] && [[ "$chart" != "$dep" ]]; then (cd "$CHARTS_DIR/$dep" && write_versions "$target_version") fi done fi + return 0 } cd "$CHARTS_DIR/$chart" && write_versions "$version" diff --git a/hack/bin/set-wire-server-image-version.sh b/hack/bin/set-wire-server-image-version.sh index 03a6024378c..8530336d574 100755 --- a/hack/bin/set-wire-server-image-version.sh +++ b/hack/bin/set-wire-server-image-version.sh @@ -6,11 +6,17 @@ target_version=${1?$USAGE} TOP_LEVEL="$( cd "$( dirname "${BASH_SOURCE[0]}" )/../.." && pwd )" CHARTS_DIR="$TOP_LEVEL/.local/charts" -charts=(brig cannon galley gundeck spar cargohold proxy cassandra-migrations elasticsearch-index federator backoffice background-worker integration wire-server-enterprise) +charts=(cassandra-migrations elasticsearch-index federator backoffice integration wire-server-enterprise) for chart in "${charts[@]}"; do - sed -i "s/^ tag: .*/ tag: $target_version/g" "$CHARTS_DIR/$chart/values.yaml" + values_file="$CHARTS_DIR/$chart/values.yaml" + if [[ -f "$values_file" ]]; then + sed -i "s/^ tag: .*/ tag: $target_version/g" "$values_file" + fi done # special case nginz sed -i "s/^ tag: .*/ tag: $target_version/g" "$CHARTS_DIR/nginz/values.yaml" + +# Brig, Galley, Cargohold, BackgroundWorker, Cannon, Proxy, Gundeck, and Spar are inlined into the umbrella chart. +sed -i "s/^ tag: .*/ tag: $target_version/g" "$CHARTS_DIR/wire-server/values.yaml" diff --git a/hack/bin/update.sh b/hack/bin/update.sh index 47ecefb2b00..36bc5c44356 100755 --- a/hack/bin/update.sh +++ b/hack/bin/update.sh @@ -14,12 +14,11 @@ dir=${1:?$USAGE} # hacky workaround for helm's lack of recursive dependency update # See https://github.com/helm/helm/issues/2247 helmDepUp () { - local path - path=$1 + local path="$1" cd "$path" # remove previous bundled versions of helm charts, if any find . -name "*\.tgz" -delete - if [ -f requirements.yaml ]; then + if [[ -f requirements.yaml ]]; then echo "Updating dependencies in $path ..." # very hacky bash, I'm sorry for subpath in $(grep "file://" requirements.yaml | awk '{ print $2 }' | xargs -n 1 | cut -c 8-) @@ -35,6 +34,7 @@ helmDepUp () { helm dep up echo "... updating in $path done." fi + return 0 } helmDepUp "$dir" diff --git a/hack/bin/upload-all-sboms.sh b/hack/bin/upload-all-sboms.sh new file mode 100755 index 00000000000..524b8fb190c --- /dev/null +++ b/hack/bin/upload-all-sboms.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Script to upload all generated SBOMs to Dependency Track +# +# The Dependency Track API is described here (openapi/swagger): +# https://editor.swagger.io/?url=https://deptrack.wire.link/api/openapi.json + +PROJECT_NAME="${1:-}" +VERSION="${2:-}" + +if [[ -z "$PROJECT_NAME" ]] || [[ -z "$VERSION" ]]; then + echo "Usage: $0 " >&2 + echo " project-name: Dependency Track project name" >&2 + echo " version: Project version" >&2 + exit 1 +fi + +if [[ -z "${DEPENDENCY_TRACK_API_KEY:-}" ]]; then + echo "ERROR: DEPENDENCY_TRACK_API_KEY environment variable not set" >&2 + exit 1 +fi + +# Find git repository root and script directory +GIT_ROOT="$(git rev-parse --show-toplevel)" +SBOMS_DIR="$GIT_ROOT/tmp/sboms" + +SCRIPT_DIR="$(dirname "$0")" + +echo "Uploading Helm SBOMs..." +find "$SBOMS_DIR/helm" -name '*.json' -type f -not -path '*/.oci-cache/*' 2>/dev/null | while read -r sbom; do + chart_purl=$(jq -r '.metadata.component.externalReferences[] | select(.comment == "Helm chart") | .url' "$sbom") + chart_name=$(echo "$chart_purl" | sed 's|pkg:helm/||' | cut -d'@' -f1) + "$SCRIPT_DIR/upload-sbom.sh" "$sbom" "Helm charts" "$PROJECT_NAME" "$VERSION" "$chart_name" || exit 1 +done + +echo "Uploading Helmfile SBOMs..." +find "$SBOMS_DIR/helmfile" -name '*.json' -type f -not -path '*/.oci-cache/*' 2>/dev/null | while read -r sbom; do + "$SCRIPT_DIR/upload-sbom.sh" "$sbom" helmfile "$PROJECT_NAME" "$VERSION" || exit 1 +done + +echo "Uploading Docker Compose SBOMs..." +find "$SBOMS_DIR/docker-compose" -name '*.json' -type f -not -path '*/.oci-cache/*' 2>/dev/null | while read -r sbom; do + "$SCRIPT_DIR/upload-sbom.sh" "$sbom" docker-compose "$PROJECT_NAME" "$VERSION" || exit 1 +done + +echo "✓ All SBOMs uploaded successfully" diff --git a/hack/bin/upload-helm-charts-s3.sh b/hack/bin/upload-helm-charts-s3.sh index 434c8986476..2e39cf2dfaf 100755 --- a/hack/bin/upload-helm-charts-s3.sh +++ b/hack/bin/upload-helm-charts-s3.sh @@ -31,7 +31,7 @@ Options: exit_usage() { echo "$USAGE" - exit 1 + return 1 } # To be somewhat backwards-compatible, transform long options to short ones @@ -113,7 +113,7 @@ CHART_DIR=$TOP_LEVEL_DIR/.local/charts cd "$TOP_LEVEL_DIR" # If ./upload-helm-charts-s3.sh is run with a parameter, only synchronize one chart -if [ -n "$chart_dir" ] && [ -d "$chart_dir" ]; then +if [[ -n "$chart_dir" ]] && [[ -d "$chart_dir" ]]; then chart_name=$(basename "$chart_dir") echo "only syncing $chart_name" charts=( "$chart_name" ) @@ -190,7 +190,7 @@ fi # echo $cur_hash # remote_hash=$(aws s3api head-object --bucket public.wire.com --key charts/${tgz} | jq '.ETag' -r| tr -d '"') # echo $remote_hash -# if [ "$cur_hash" != "$remote_hash" ]; then +# if [[ "$cur_hash" != "$remote_hash" ]]; then # echo "ERROR: Current hash should be the same as the remote hash. Please bump the version of chart {$chart}." # exit 1 # fi diff --git a/hack/bin/upload-image.sh b/hack/bin/upload-image.sh index 080c18d8dcc..e7300e11777 100755 --- a/hack/bin/upload-image.sh +++ b/hack/bin/upload-image.sh @@ -28,26 +28,26 @@ fi # Also, skopeo's retry logic doesn't properly work, look here if you want to see very badly written go code: # https://github.com/containers/skopeo/blob/869d496f185cc086f22d6bbb79bb57ac3a415617/vendor/github.com/containers/common/pkg/retry/retry.go#L52-L113 function retry { - local maxAttempts=$1 - local secondsDelay=1 - local attemptCount=1 + local max_attempts=$1 + local seconds_delay=1 + local attempt_count=1 shift 1 - while [ $attemptCount -le "$maxAttempts" ]; do + while [[ $attempt_count -le "$max_attempts" ]]; do if "$@"; then break else local status=$? - if [ $attemptCount -lt "$maxAttempts" ]; then - echo "Command [$*] failed after attempt $attemptCount of $maxAttempts. Retrying in $secondsDelay second(s)." >&2 - sleep $secondsDelay - elif [ $attemptCount -eq "$maxAttempts" ]; then - echo "Command [$*] failed after $attemptCount attempt(s)" >&2 + if [[ $attempt_count -lt "$max_attempts" ]]; then + echo "Command [$*] failed after attempt $attempt_count of $max_attempts. Retrying in $seconds_delay second(s)." >&2 + sleep $seconds_delay + elif [[ $attempt_count -eq "$max_attempts" ]]; then + echo "Command [$*] failed after $attempt_count attempt(s)" >&2 return $status fi fi - attemptCount=$((attemptCount + 1)) - secondsDelay=$((secondsDelay * 2)) + attempt_count=$((attempt_count + 1)) + seconds_delay=$((seconds_delay * 2)) done } diff --git a/hack/bin/upload-sbom.sh b/hack/bin/upload-sbom.sh new file mode 100755 index 00000000000..8a6a4a0d748 --- /dev/null +++ b/hack/bin/upload-sbom.sh @@ -0,0 +1,369 @@ +#!/usr/bin/env bash + +# Upload a SBOM file and ensure the project structure exists. +# +# The Dependency Track API is described here (openapi/swagger): +# https://editor.swagger.io/?url=https://deptrack.wire.link/api/openapi.json + + +set -euo pipefail + +# Constants +API_BASE="https://deptrack.wire.link/api/v1" + +# Parse arguments +SBOM_FILE="${1:-}" +SOURCE_NAME="${2:-}" +PARENT_PROJECT_NAME="${3:-}" +PARENT_PROJECT_VERSION="${4:-}" +CHART_NAME="${5:-}" + +if [[ -z "$SBOM_FILE" ]] || [[ -z "$SOURCE_NAME" ]] || [[ -z "$PARENT_PROJECT_NAME" ]] || [[ -z "$PARENT_PROJECT_VERSION" ]]; then + echo "Usage: $0 [chart-name]" + echo " sbom-file: Path to SBOM JSON file to upload" + echo " source-name: Source type (e.g., 'Helm charts', 'helmfile', 'docker-compose')" + echo " parent-project-name: Parent project name (required)" + echo " parent-project-version: Parent project version (required)" + echo " chart-name: Chart name for Helm charts (optional, will prefix project name)" + echo "" + echo "Environment variables:" + echo " DEPENDENCY_TRACK_API_KEY: API key for Dependency Track (required)" + exit 1 +fi + +if [[ ! -f "$SBOM_FILE" ]]; then + echo "ERROR: SBOM file not found: $SBOM_FILE" >&2 + exit 1 +fi + +if [[ -z "${DEPENDENCY_TRACK_API_KEY:-}" ]]; then + echo "ERROR: DEPENDENCY_TRACK_API_KEY environment variable not set" >&2 + exit 1 +fi + +# ============================================================================ +# Functions +# ============================================================================ + +# Lookup a project by name and version +# Returns: project JSON if found, empty string otherwise +lookup_project() { + local name="$1" + local version="$2" + + local response + response=$(curl -s -w '\n%{http_code}' -X GET \ + "${API_BASE}/project/lookup?name=$(printf %s "$name" | jq -sRr @uri)&version=$(printf %s "$version" | jq -sRr @uri)" \ + -H "X-Api-Key: $DEPENDENCY_TRACK_API_KEY") + + local status="${response##*$'\n'}" + local body="${response%$'\n'*}" + + if [[ "$status" == "200" ]]; then + local uuid + uuid=$(echo "$body" | jq -r '.uuid // empty') + if [[ -n "$uuid" ]]; then + echo "$body" + return 0 + fi + fi + + return 1 +} + +# Create a new project +# Args: name, version, parent_uuid (optional) +# Returns: project UUID +create_project() { + local name="$1" + local version="$2" + # No $parent_uuid means: Top-level project + local parent_uuid="${3:-}" + + local payload + if [[ -n "$parent_uuid" ]]; then + payload=$(jq -n \ + --arg name "$name" \ + --arg version "$version" \ + --arg parentUuid "$parent_uuid" \ + '{ + name: $name, + version: $version, + classifier: "APPLICATION", + collectionLogic: "AGGREGATE_DIRECT_CHILDREN", + parent: {uuid: $parentUuid}, + active: true + }') + else + payload=$(jq -n \ + --arg name "$name" \ + --arg version "$version" \ + '{ + name: $name, + version: $version, + classifier: "APPLICATION", + collectionLogic: "AGGREGATE_DIRECT_CHILDREN", + active: true + }') + fi + + local response + response=$(curl -s -w '\n%{http_code}' -X PUT "${API_BASE}/project" \ + -H "X-Api-Key: $DEPENDENCY_TRACK_API_KEY" \ + -H "Content-Type: application/json" \ + -d "$payload") + + local status="${response##*$'\n'}" + local body="${response%$'\n'*}" + + if [[ "$status" == "201" ]]; then + local uuid + uuid=$(echo "$body" | jq -r '.uuid // empty') + if [[ -n "$uuid" ]]; then + echo "$uuid" + return 0 + fi + fi + + echo "ERROR: Failed to create project (HTTP $status)" >&2 + echo "$body" >&2 + return 1 +} + +# Check if project exists, create if not +# Args: name, version, parent_uuid (optional), description +# Returns: project UUID (to stdout) +check_or_create_project() { + local name="$1" + local version="$2" + local parent_uuid="${3:-}" + local description="$4" + + echo "Checking $description..." >&2 + + local project + if project=$(lookup_project "$name" "$version"); then + local uuid + uuid=$(echo "$project" | jq -r '.uuid') + echo "✓ $description exists: $uuid" >&2 + echo "$uuid" + return 0 + fi + + echo "$description not found, creating..." >&2 + local uuid + if uuid=$(create_project "$name" "$version" "$parent_uuid"); then + echo "✓ $description created: $uuid" >&2 + echo "$uuid" + return 0 + fi + + return 1 +} + +# Upload BOM to Dependency Track using multipart/form-data +# Args: project_name, project_version, parent_name, parent_version, bom_file +upload_bom() { + local project_name="$1" + local project_version="$2" + local parent_name="$3" + local parent_version="$4" + local bom_file="$5" + + echo "Uploading BOM..." + echo " projectName: $project_name" + echo " projectVersion: $project_version" + echo " parentName: $parent_name" + echo " parentVersion: $parent_version" + echo "" + + local response_file + response_file=$(mktemp) + trap 'rm -f "$response_file"' RETURN + + local http_code + http_code=$(curl -s -w '%{http_code}' -o "$response_file" -X POST "${API_BASE}/bom" \ + -H "X-Api-Key: $DEPENDENCY_TRACK_API_KEY" \ + -F "projectName=$project_name" \ + -F "projectVersion=$project_version" \ + -F "parentName=$parent_name" \ + -F "parentVersion=$parent_version" \ + -F "autoCreate=true" \ + -F "bom=@$bom_file") + + echo "HTTP Status: $http_code" + + if [[ "$http_code" != "200" ]]; then + echo "✗ BOM upload failed (HTTP $http_code)" >&2 + cat "$response_file" >&2 + return 1 + fi + + echo "✓ BOM upload successful" + local token + token=$(jq -r '.token // empty' "$response_file") + if [[ -n "$token" ]]; then + echo " Processing token: $token" + fi + echo "" + + return 0 +} + +# Update project parent relationship and external refs +# Args: child_uuid, parent_uuid, external_refs_json +update_parent_and_external_refs() { + local child_uuid="$1" + local parent_uuid="$2" + local external_refs="$3" + + echo "Setting parent relationship..." + + # Get full project details + local full_project + full_project=$(curl -s -X GET \ + "${API_BASE}/project/${child_uuid}" \ + -H "X-Api-Key: $DEPENDENCY_TRACK_API_KEY") + + # Build the update payload + local update_payload + update_payload=$(echo "$full_project" | jq \ + --arg parentUuid "$parent_uuid" \ + --argjson externalReferences "$external_refs" \ + '. + {parent: {uuid: $parentUuid}} + | if $externalReferences != null then . + {externalReferences: $externalReferences} else . end') + + # Update with parent UUID + local response + response=$(echo "$update_payload" \ + | curl -s -w '\n%{http_code}' -X POST "${API_BASE}/project" \ + -H "X-Api-Key: $DEPENDENCY_TRACK_API_KEY" \ + -H "Content-Type: application/json" \ + -d @-) + + local status="${response##*$'\n'}" + local body="${response%$'\n'*}" + + if [[ "$status" == "200" ]]; then + echo "✓ Parent relationship set" + return 0 + fi + + echo "✗ Failed to set parent relationship (HTTP $status)" >&2 + echo "$body" >&2 + return 1 +} + +# Lookup and verify child project exists +# Args: project_name, project_version +# Returns: child_uuid (to stdout) +lookup_child_project() { + local project_name="$1" + local project_version="$2" + + echo "Looking up child project '$project_name'..." >&2 + + local child_project + child_project=$(lookup_project "$project_name" "$project_version") + if [[ -z "$child_project" ]]; then + echo "✗ Child project not found after BOM upload" >&2 + exit 1 + fi + + local child_uuid + child_uuid=$(echo "$child_project" | jq -r '.uuid') + echo "✓ Child project exists: $child_uuid" >&2 + echo "" >&2 + + echo "$child_uuid" + return 0 +} + +# Verify child appears in parent's children list +# Args: parent_uuid, child_project_name +verify_child_in_parent() { + local parent_uuid="$1" + local child_name="$2" + + echo "Verifying child appears in source project's children..." + + local children_response + children_response=$(curl -s -X GET \ + "${API_BASE}/project/${parent_uuid}/children" \ + -H "X-Api-Key: $DEPENDENCY_TRACK_API_KEY") + + local child_uuid + child_uuid=$(echo "$children_response" | jq -r --arg projectName "$child_name" \ + '.[] | select(.name == $projectName) | .uuid // empty') + + if [[ -n "$child_uuid" ]]; then + echo "✓ Child project '$child_name' found in source project's children" + return 0 + fi + + echo "✗ Child project not found in source project's children list" >&2 + echo " Children: $(echo "$children_response" | jq -c 'map(.name)')" >&2 + return 1 +} + +# ============================================================================ +# Main Script +# ============================================================================ + +# Extract metadata from SBOM +purl=$(jq -r '.metadata.component.purl // empty' "$SBOM_FILE") +component_name=$(jq -r '.metadata.component.name // empty' "$SBOM_FILE") +component_version=$(jq -r '.metadata.component.version // empty' "$SBOM_FILE") + +if [[ -z "$purl" ]]; then + echo "ERROR: No purl found in SBOM metadata.component.purl" >&2 + exit 1 +fi + +# Use component version or default to "unknown" +PROJECT_VERSION="${component_version:-unknown}" + +# Construct project name: for Helm charts, prefix with chart name +if [[ -n "$CHART_NAME" ]]; then + PROJECT_NAME="$CHART_NAME: $purl" +else + PROJECT_NAME="$purl" +fi + +# Extract externalReferences +external_refs=$(jq -c '.metadata.component.externalReferences // null' "$SBOM_FILE") + +echo "Uploading SBOM: $SBOM_FILE" +echo " Component: $component_name" +echo " Source: $SOURCE_NAME" +echo " Project Name: $PROJECT_NAME" +[[ -n "$CHART_NAME" ]] && echo " Chart: $CHART_NAME" +echo " Purl: $purl" +echo " Project Version: $PROJECT_VERSION" +echo " Parent: $PARENT_PROJECT_NAME @ $PARENT_PROJECT_VERSION" +[[ "$external_refs" != "null" ]] && echo " External References: $(echo "$external_refs" | jq 'length')" +echo "" + +# Step 1: Check/create parent project +parent_uuid=$(check_or_create_project "$PARENT_PROJECT_NAME" "$PARENT_PROJECT_VERSION" "" "parent project") +echo "" + +# Step 2: Check/create source project (intermediate level) +source_uuid=$(check_or_create_project "$SOURCE_NAME" "$PARENT_PROJECT_VERSION" "$parent_uuid" "source project") +echo "" + +# Step 3: Upload BOM using multipart/form-data (supports large files) +upload_bom "$PROJECT_NAME" "$PROJECT_VERSION" "$SOURCE_NAME" "$PARENT_PROJECT_VERSION" "$SBOM_FILE" + +# Step 4: Lookup and verify child project +child_uuid=$(lookup_child_project "$PROJECT_NAME" "$PROJECT_VERSION") + +# Step 5: Fixup parent relationship and set external refs +# These are unfortunately not ensured during SBOM uploading +update_parent_and_external_refs "$child_uuid" "$source_uuid" "$external_refs" + +# Step 6: Verify child appears in source project's children +verify_child_in_parent "$source_uuid" "$PROJECT_NAME" +echo "" + +echo "✓ Upload completed successfully" diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index 3745be436a9..5255592075c 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -1,18 +1,13 @@ tags: nginz: true - brig: true - galley: true - gundeck: true - cannon: true - cargohold: true - spar: true - federation: true # also see {background-worker,brig,cargohold,galley}.config.enableFederation + federation: true backoffice: true - proxy: true legalhold: false sftd: false integration: true +consumableNotifications: false + cassandra-migrations: imagePullPolicy: {{ .Values.imagePullPolicy }} cassandra: @@ -554,11 +549,12 @@ proxy: secrets: proxy_config: |- secrets { - youtube = "..." - googlemaps = "..." - soundcloud = "..." - giphy = "..." - spotify = "Basic ..." + youtube = "my-youtube-secret" + googlemaps = "my-googlemaps-secret" + soundcloud = "my-soundcloud-secret" + giphy = "my-giphy-secret" + # Base64 encoded client ID and secret: `Bearer id:secret`: + spotify = "my-spotify-secret" } config: disabledAPIVersions: [] @@ -672,6 +668,7 @@ background-worker: name: "cassandra-jks-keystore" key: "ca.crt" {{- end }} + # See helmfile for the real value federationDomain: integration.example.com postgresMigration: conversation: {{ .Values.conversationStore }} @@ -690,7 +687,6 @@ background-worker: rabbitmq: username: {{ .Values.rabbitmqUsername }} password: {{ .Values.rabbitmqPassword }} - pgPassword: "posty-the-gres" integration: ingress: @@ -741,16 +737,3 @@ backoffice: uploadXmlAwsAccessKeyId: {{ .Values.uploadXml.awsAccessKeyId }} uploadXmlAwsSecretAccessKey: {{ .Values.uploadXml.awsSecretAccessKey }} {{- end }} - -proxy: - replicaCount: 1 - secrets: - proxy_config: | - secrets { - youtube = "my-youtube-secret" - googlemaps = "my-googlemaps-secret" - soundcloud = "my-soundcloud-secret" - giphy = "my-giphy-secret" - # Base64 encoded client ID and secret: `Bearer id:secret`: - spotify = "my-spotify-secret" - } diff --git a/hack/helmfile.yaml.gotmpl b/hack/helmfile.yaml.gotmpl index b5afd76bf76..c64cc8bbf46 100644 --- a/hack/helmfile.yaml.gotmpl +++ b/hack/helmfile.yaml.gotmpl @@ -324,9 +324,9 @@ releases: value: {{ .Values.federationDomain1 }} - name: galley.config.settings.federationDomain value: {{ .Values.federationDomain1 }} - - name: cargohold.config.settings.federationDomain + - name: backgroundWorker.config.federationDomain value: {{ .Values.federationDomain1 }} - - name: background-worker.config.federationDomain + - name: cargohold.config.settings.federationDomain value: {{ .Values.federationDomain1 }} - name: brig.config.wireServerEnterprise.enabled value: true @@ -346,9 +346,9 @@ releases: value: {{ .Values.federationDomain2 }} - name: galley.config.settings.federationDomain value: {{ .Values.federationDomain2 }} - - name: cargohold.config.settings.federationDomain + - name: backgroundWorker.config.federationDomain value: {{ .Values.federationDomain2 }} - - name: background-worker.config.federationDomain + - name: cargohold.config.settings.federationDomain value: {{ .Values.federationDomain2 }} needs: - 'cassandra-ephemeral' diff --git a/hack/python/wire/api.py b/hack/python/wire/api.py index aea4ce10e8f..256d2936c1f 100644 --- a/hack/python/wire/api.py +++ b/hack/python/wire/api.py @@ -13,6 +13,7 @@ DEFAULT_PASSWORD = "hunter2!" +MESSAGE_MLS_CONTENT_TYPE = "message/mls" def random_letters(n=10): return "".join(random.choices(string.ascii_letters, k=n)) @@ -36,7 +37,7 @@ def create_user(ctx, email=None, password=None, name=None, create_team=False, ** if create_team: body["team"] = {"name": "Wire team 2", "icon": "default"} - for k, v in kwargs: + for k, v in kwargs.items(): body[k] = v url = ctx.mkurl("brig", "/i/users", internal=True) @@ -80,7 +81,7 @@ def put_client_mls_public_keys(ctx, client_id, mls_public_keys): return ctx.request("PUT", url, json=body) -def delete_client(ctx, user, client_id, password=DEFAULT_PASSWORD): +def delete_client(ctx, user, password=DEFAULT_PASSWORD): url = ctx.mkurl("brig", f"/clients/{obj_id(user)}") return ctx.request("DELETE", url, user=user, json={"password": password}) @@ -116,30 +117,30 @@ def get_conversation(ctx, user, conv): return ctx.request("GET", url, user=user) -def mls_get_public_keys(ctx, **additional_args): +def mls_get_public_keys(ctx): url = ctx.mkurl("galley", "/mls/public-keys") return ctx.request("GET", url) def mls_message(ctx, user, message): - headers = {"Content-Type": "message/mls"} + headers = {"Content-Type": MESSAGE_MLS_CONTENT_TYPE} url = ctx.mkurl("galley", "/mls/messages") return ctx.request("POST", url, user=user, data=message, headers=headers) def mls_welcome(ctx, user, welcome): - headers = {"Content-Type": "message/mls"} + headers = {"Content-Type": MESSAGE_MLS_CONTENT_TYPE} url = ctx.mkurl("galley", "/mls/welcome") return ctx.request("POST", url, user=user, data=welcome, headers=headers) def mls_post_commit_bundle(ctx, client, commit_bundle): - url = ctx.mkurl("galley", f"/mls/commit-bundles") + url = ctx.mkurl("galley", "/mls/commit-bundles") tbefore = time.time() res = ctx.request( "POST", url, - headers={"Content-Type": "message/mls"}, + headers={"Content-Type": MESSAGE_MLS_CONTENT_TYPE}, client=client, data=commit_bundle, ) @@ -149,8 +150,8 @@ def mls_post_commit_bundle(ctx, client, commit_bundle): def mls_send_message(ctx, msg, **kwargs): - headers = {"Content-Type": "message/mls"} - url = ctx.mkurl("galley", f"/mls/messages") + headers = {"Content-Type": MESSAGE_MLS_CONTENT_TYPE} + url = ctx.mkurl("galley", "/mls/messages") return ctx.request("POST", url, headers=headers, data=msg, **kwargs) @@ -172,7 +173,7 @@ def remove_member(ctx, user, conv, target, **kwargs): return ctx.request("DELETE", url, user=user) -def login(ctx, email, password=DEFAULT_PASSWORD, **additional_args): +def login(ctx, email, password=DEFAULT_PASSWORD): body = {"email": email, "password": password} url = ctx.mkurl("brig", "/login") return ctx.request("POST", url, json=body) @@ -189,7 +190,7 @@ def create_access_token(ctx, client_id=None): return ctx.request("POST", url, params=params) -def create_team_invitation(ctx, team, email_invite=None, user=None, **additional_args): +def create_team_invitation(ctx, team, email_invite=None, user=None): if email_invite is None: email_invite = random_email() url = ctx.mkurl("brig", f"/teams/{team}/invitations") diff --git a/hack/python/wire/mlscli.py b/hack/python/wire/mlscli.py index 42b71b459a5..196f0f53f7f 100644 --- a/hack/python/wire/mlscli.py +++ b/hack/python/wire/mlscli.py @@ -9,28 +9,43 @@ import shutil import pickle import uuid +import hashlib +# Constants for template placeholders +GROUP_IN_PLACEHOLDER = "" +GROUP_OUT_PLACEHOLDER = "" def cid2str(client_identity): - u = client_identity["user"] - c = client_identity["client"] - d = client_identity["domain"] + # Sanitize path components to prevent path injection + def sanitize_path_component(s): + # Replace dangerous characters that could be used for path traversal + return str(s).replace('/', '_').replace('\\', '_').replace('..', '__').replace(':', '_') + u = sanitize_path_component(client_identity["user"]) + c = sanitize_path_component(client_identity["client"]) + d = sanitize_path_component(client_identity["domain"]) return f"{u}:{c}@{d}" -def mlscli(state, client_identity, args, stdin=None): +def safe_client_dir_name(client_identity): + # Create a safe directory name using hash of client identity + # This prevents path injection while maintaining uniqueness + identity_str = json.dumps(client_identity, sort_keys=True) + return hashlib.sha256(identity_str.encode('utf-8')).hexdigest()[:16] + + +def mlscli(state, args, stdin=None): cdir = state.client_dir subst = {} - if "" in args: + if GROUP_IN_PLACEHOLDER in args: group_in_file = random_path(state) with open(group_in_file, "wb") as f: f.write(state.group_state) - subst[""] = group_in_file + subst[GROUP_IN_PLACEHOLDER] = group_in_file - want_group_out = "" in args + want_group_out = GROUP_OUT_PLACEHOLDER in args if want_group_out: - subst[""] = random_path(state) + subst[GROUP_OUT_PLACEHOLDER] = random_path(state) args_substd = [] for arg in args: @@ -67,7 +82,7 @@ def mlscli(state, client_identity, args, stdin=None): raise ValueError(msg) if want_group_out: - with open(subst[""], "br") as f: + with open(subst[GROUP_OUT_PLACEHOLDER], "br") as f: state.group_state = f.read() return stdout_data @@ -161,24 +176,24 @@ def __repr__(self): return f'{self.__class__.__name__}({values})' def key_package_file(state, ref): - return os.path.join(state.client_dir, cid2str(state.client_identity), ref.hex()) + return os.path.join(state.client_dir, safe_client_dir_name(state.client_identity), ref.hex()) def init_mls_client(state): # the arg after 'init' determines will be clientidentity in all keypackages created with the cli thereafter - mlscli(state, state.client_identity, ["init", cid2str(state.client_identity)]) + mlscli(state, ["init", cid2str(state.client_identity)]) def get_public_key(state): - return mlscli(state, state.client_identity, ["public-key"]) + return mlscli(state, ["public-key"]) def generate_key_package(state): - kp = mlscli(state, state.client_identity, ["key-package", "create"]) + kp = mlscli(state, ["key-package", "create"]) kp_path = random_path(state) with open(kp_path, "wb") as f: f.write(kp) - ref = mlscli(state, state.client_identity, ["key-package", "ref", kp_path]) + ref = mlscli(state, ["key-package", "ref", kp_path]) dest = key_package_file(state, ref) shutil.move(kp_path, dest) @@ -186,7 +201,7 @@ def generate_key_package(state): def create_group(state, group_id): - group_state = mlscli(state, state.client_identity, ["group", "create", group_id]) + group_state = mlscli(state, ["group", "create", group_id]) state.group_state = group_state @@ -199,16 +214,16 @@ def add_member(state, kpfiles): "member", "add", "--group", - "", + GROUP_IN_PLACEHOLDER, "--welcome-out", welcome_file, "--group-info-out", pgs_file, "--group-out", - "", + GROUP_OUT_PLACEHOLDER, ] + kpfiles - msg = mlscli(state, state.client_identity, args) + msg = mlscli(state, args) welcome = b"" if os.path.exists(welcome_file): @@ -247,10 +262,10 @@ def consume_welcome(state, welcome): "group", "from-welcome", "--group-out", - "", + GROUP_OUT_PLACEHOLDER, "-", ] - mlscli(state, state.client_identity, args, stdin=welcome) + mlscli(state, args, stdin=welcome) def consume_message(state, msg, removal_key_file, ignore_stale=False): @@ -258,21 +273,21 @@ def consume_message(state, msg, removal_key_file, ignore_stale=False): [ "consume", "--group", - "", + GROUP_IN_PLACEHOLDER, "--group-out", - "", + GROUP_OUT_PLACEHOLDER, "--signer-key", removal_key_file, ] + (["--ignore-stale"] if ignore_stale else []) + ["-"] ) - return mlscli(state, state.client_identity, args, stdin=msg) + return mlscli(state, args, stdin=msg) def create_application_message(state, message_content): - args = ["message", "--group", "", message_content] - msg = mlscli(state, state.client_identity, args) + args = ["message", "--group", GROUP_IN_PLACEHOLDER, message_content] + msg = mlscli(state, args) message_package = { "sender": state.client_identity, "message": msg, diff --git a/hack/python/wire/response.py b/hack/python/wire/response.py index d7a70459404..ebb8a2f5b1d 100644 --- a/hack/python/wire/response.py +++ b/hack/python/wire/response.py @@ -9,7 +9,7 @@ def __init__(self, method, url, request, response): self.method = method self.url = url self.request = request - self.response = response + self.http_response = response def __enter__(self): return self @@ -28,13 +28,13 @@ def debug(self): print(json.dumps(req, indent=2)) # print response status code and JSON if present - print("status code:", self.response.status_code) + print("status code:", self.http_response.status_code) print("response body:") try: - resp = self.response.json() + resp = self.http_response.json() print(json.dumps(resp, indent=2)) except requests.exceptions.JSONDecodeError: - print(self.response.text) + print(self.http_response.text) def check(self, prop=None, *, status=None): with self: @@ -46,12 +46,12 @@ def check(self, prop=None, *, status=None): @property def status_code(self): - return self.response.status_code + return self.http_response.status_code @property def text(self): - return self.response.text + return self.http_response.text def json(self): with self: - return self.response.json(object_hook=frozendict) + return self.http_response.json(object_hook=frozendict) diff --git a/integration/integration.cabal b/integration/integration.cabal index 88fede39a13..2a4fb71b60d 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -158,6 +158,7 @@ library Test.FeatureFlags.MlsE2EId Test.FeatureFlags.MlsMigration Test.FeatureFlags.OutlookCalIntegration + Test.FeatureFlags.RequireExternalEmailVerification Test.FeatureFlags.SearchVisibilityAvailable Test.FeatureFlags.SearchVisibilityInbound Test.FeatureFlags.SelfDeletingMessages @@ -167,7 +168,6 @@ library Test.FeatureFlags.StealthUsers Test.FeatureFlags.User Test.FeatureFlags.Util - Test.FeatureFlags.ValidateSAMLEmails Test.Federation Test.Federator Test.LegalHold diff --git a/integration/scripts/integration-dynamic-backends-db-schemas.sh b/integration/scripts/integration-dynamic-backends-db-schemas.sh index aea3c4ddb85..0b0ca02e9f7 100755 --- a/integration/scripts/integration-dynamic-backends-db-schemas.sh +++ b/integration/scripts/integration-dynamic-backends-db-schemas.sh @@ -9,11 +9,17 @@ genargs() { echo "$service"/"$service"_test_dyn_"$i" done done + return 0 } migrate_schema() { - cmd="$1-schema --keyspace $2 ${*:3}" + local service="$1" + local keyspace="$2" + shift 2 + local extra_args="$*" + cmd="$service-schema --keyspace $keyspace $extra_args" $cmd + return $? } export -f migrate_schema diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index 9978591c46d..cecb6fbef73 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -1218,10 +1218,8 @@ getAllTeamCollaborators owner tid = do data NewApp = NewApp { name :: String, - pict :: Maybe [Value], assets :: Maybe [Value], accentId :: Maybe Int, - meta :: Value, category :: String, description :: String } @@ -1230,10 +1228,8 @@ instance Default NewApp where def = NewApp { name = "", - pict = Nothing, assets = Nothing, accentId = Nothing, - meta = object [], category = "other", description = "" } @@ -1244,16 +1240,11 @@ createApp creator tid new = do submit "POST" $ req & addJSONObject - [ "app" - .= object - [ "name" .= new.name, - "picture" .= new.pict, - "assets" .= new.assets, - "accent_id" .= new.accentId, - "metadata" .= new.meta, - "category" .= new.category, - "description" .= new.description - ], + [ "name" .= new.name, + "assets" .= new.assets, + "accent_id" .= new.accentId, + "category" .= new.category, + "description" .= new.description, "password" .= defPassword ] @@ -1275,10 +1266,10 @@ putAppMetadata tid owner appId appMetadata = do req <- baseRequest owner Brig Versioned path submit "PUT" (req & addJSON appMetadata) -refreshAppCookie :: (MakesValue u) => u -> String -> String -> App Response -refreshAppCookie u tid appId = do +refreshAppCookie :: (MakesValue u) => u -> String -> String -> Maybe Value -> App Response +refreshAppCookie u tid appId mbBody = do req <- baseRequest u Brig Versioned $ joinHttpPath ["teams", tid, "apps", appId, "cookies"] - submit "POST" req + submit "POST" $ req & maybe id addJSON mbBody -- | https://staging-nginz-https.zinfra.io/v12/api/swagger-ui/#/default/check-user-handle checkHandle :: (MakesValue user) => user -> String -> App Response diff --git a/integration/test/API/BrigInternal.hs b/integration/test/API/BrigInternal.hs index 03775fc5144..ff9ab3d1357 100644 --- a/integration/test/API/BrigInternal.hs +++ b/integration/test/API/BrigInternal.hs @@ -414,3 +414,15 @@ getMLSClients user ciphersuite = do userId <- objId user req <- baseRequest user Brig Unversioned $ joinHttpPath ["i", "mls", "clients", userId] submit "GET" $ req & addQueryParams [("ciphersuite", ciphersuite.code)] + +setAccountStatus :: (HasCallStack, MakesValue user) => user -> String -> App Response +setAccountStatus user status = do + userId <- objId user + req <- baseRequest user Brig Unversioned $ joinHttpPath ["i", "users", userId, "status"] + submit "PUT" $ req & addJSONObject ["status" .= status] + +getAccountStatus :: (HasCallStack, MakesValue user) => user -> App Response +getAccountStatus user = do + userId <- objId user + req <- baseRequest user Brig Unversioned $ joinHttpPath ["i", "users", userId, "status"] + submit "GET" req diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index 2726bcc0628..1b3ca8dd677 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -1003,3 +1003,18 @@ getMeeting :: (HasCallStack, MakesValue user) => user -> String -> String -> App getMeeting user domain meetingId = do req <- baseRequest user Galley Versioned (joinHttpPath ["meetings", domain, meetingId]) submit "GET" req + +getMeetingsList :: (HasCallStack, MakesValue user) => user -> App Response +getMeetingsList user = do + req <- baseRequest user Galley Versioned "/meetings/list" + submit "GET" req + +postMeetingInvitation :: (HasCallStack, MakesValue user) => user -> String -> String -> Aeson.Value -> App Response +postMeetingInvitation user domain meetingId invitation = do + req <- baseRequest user Galley Versioned (joinHttpPath ["meetings", domain, meetingId, "invitations"]) + submit "POST" $ req & addJSON invitation + +deleteMeetingInvitation :: (HasCallStack, MakesValue user) => user -> String -> String -> Aeson.Value -> App Response +deleteMeetingInvitation user domain meetingId removeInvitation = do + req <- baseRequest user Galley Versioned (joinHttpPath ["meetings", domain, meetingId, "invitations", "delete"]) + submit "POST" $ req & addJSON removeInvitation diff --git a/integration/test/API/Spar.hs b/integration/test/API/Spar.hs index e3033248cbe..b3089f1b778 100644 --- a/integration/test/API/Spar.hs +++ b/integration/test/API/Spar.hs @@ -168,14 +168,26 @@ 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 = (flip createIdpWithZHost) Nothing +createIdp = (flip createIdpWithZHostV2) Nothing -createIdpWithZHost :: (HasCallStack, MakesValue user) => user -> Maybe String -> SAML.IdPMetadata -> App Response -createIdpWithZHost user mbZHost metadata = do +-- | Create an IdP using API version V2. +-- +-- V2 enforces issuer uniqueness per team (issuers can be reused across different teams). +createIdpWithZHostV2 :: (HasCallStack, MakesValue user) => user -> Maybe String -> SAML.IdPMetadata -> App Response +createIdpWithZHostV2 = createIdpWithZHostAndVersion "v2" + +-- | Create an IdP using API version V1. +-- +-- V1 enforces global issuer uniqueness across the entire backend (all teams). +createIdpWithZHostV1 :: (HasCallStack, MakesValue user) => user -> Maybe String -> SAML.IdPMetadata -> App Response +createIdpWithZHostV1 = createIdpWithZHostAndVersion "v1" + +createIdpWithZHostAndVersion :: (HasCallStack, MakesValue user) => String -> user -> Maybe String -> SAML.IdPMetadata -> App Response +createIdpWithZHostAndVersion apiVersion user mbZHost metadata = do bReq <- baseRequest user Spar Versioned "/identity-providers" let req = bReq - & addQueryParams [("api_version", "v2")] + & addQueryParams [("api_version", apiVersion)] & addXML (fromLT $ SAML.encode metadata) & addHeader "Content-Type" "application/xml" submit "POST" (req & maybe id zHost mbZHost) diff --git a/integration/test/SetupHelpers.hs b/integration/test/SetupHelpers.hs index 590e3bd6c8f..02cb9717b4d 100644 --- a/integration/test/SetupHelpers.hs +++ b/integration/test/SetupHelpers.hs @@ -498,7 +498,7 @@ registerTestIdPWithMetaWithPrivateCredsForZHost :: App (Response, (SAML.IdPMetadata, SAML.SignPrivCreds)) registerTestIdPWithMetaWithPrivateCredsForZHost owner mbZhost = do SampleIdP idpmeta pCreds _ _ <- makeSampleIdPMetadata - (,(idpmeta, pCreds)) <$> createIdpWithZHost owner mbZhost idpmeta + (,(idpmeta, pCreds)) <$> createIdpWithZHostV2 owner mbZhost idpmeta registerTestIdPWithMetaWithPrivateCreds :: (HasCallStack, MakesValue owner) => owner -> App (Response, (SAML.IdPMetadata, SAML.SignPrivCreds)) registerTestIdPWithMetaWithPrivateCreds = flip registerTestIdPWithMetaWithPrivateCredsForZHost Nothing diff --git a/integration/test/Test/Apps.hs b/integration/test/Test/Apps.hs index 6020d732aaf..a82af79adc6 100644 --- a/integration/test/Test/Apps.hs +++ b/integration/test/Test/Apps.hs @@ -21,6 +21,7 @@ module Test.Apps where import API.Brig import qualified API.BrigInternal as BrigI +import API.Common import API.Galley import Data.Aeson.QQ.Simple import SetupHelpers @@ -87,24 +88,26 @@ testCreateApp = do 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" + (resp.json %. "app.description") `shouldMatch` "some description of this app" + (resp.json %. "app.category") `shouldMatch` "ai" -- A teamless user can't get the app outsideUser <- randomUser domain def bindResponse (getApp outsideUser tid appId) $ \resp -> do - resp.status `shouldMatchInt` 403 - resp.json %. "label" `shouldMatch` "app-no-permission" + -- this may change soon, see + -- https://wearezeta.atlassian.net/browse/WPB-23995, + -- https://wearezeta.atlassian.net/browse/WPB-23840 + resp.status `shouldMatchInt` 200 - -- 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 + bindResponse (getApp owner2 tid appId) $ \resp -> resp.status `shouldMatchInt` 200 + bindResponse (getApp owner2 tid2 appId) $ \resp -> resp.status `shouldMatchInt` 200 + bindResponse (getApp regularMember2 tid appId) $ \resp -> resp.status `shouldMatchInt` 200 - -- Category must be any of the values for the Category enum + -- Category can be any text; sanitization must happen by clients. void $ bindResponse (createApp owner tid new {category = "notinenum"}) $ \resp -> do - resp.status `shouldMatchInt` 400 + resp.status `shouldMatchInt` 200 + deleteTeamMember tid owner (resp.json %. "user") >>= assertSuccess let foundUserType :: (HasCallStack) => Value -> String -> [String] -> App () foundUserType searcher exactMatchTerm aTypes = @@ -113,6 +116,7 @@ testCreateApp = do foundDocs :: [Value] <- resp.json %. "documents" >>= asList docsInTeam :: [Value] <- do -- make sure that matches from previous test runs don't get in the way. + -- related: https://wearezeta.atlassian.net/browse/WPB-23995 catMaybes <$> forM foundDocs @@ -142,6 +146,7 @@ testRefreshAppCookie = do charlie <- randomUser OwnDomain def let new = def {name = "flexo"} :: NewApp + goodPassword = Just (object ["password" .= defPassword]) (appId, cookie) <- bindResponse (createApp alice tid new) $ \resp -> do resp.status `shouldMatchInt` 200 @@ -149,24 +154,37 @@ testRefreshAppCookie = do cookie <- resp.json %. "cookie" & asString pure (appId, cookie) - bindResponse (refreshAppCookie bob tid appId) $ \resp -> do + bindResponse (refreshAppCookie bob tid appId goodPassword) $ \resp -> do resp.status `shouldMatchInt` 403 resp.json %. "label" `shouldMatch` "app-no-permission" - bindResponse (refreshAppCookie charlie tid appId) $ \resp -> do + bindResponse (refreshAppCookie charlie tid appId goodPassword) $ \resp -> do resp.status `shouldMatchInt` 403 resp.json %. "label" `shouldMatch` "app-no-permission" - cookie' <- bindResponse (refreshAppCookie alice tid appId) $ \resp -> do + forM_ + [ (Nothing, 415), + (Just Null, 400), + (Just (object []), 403), + (Just (object ["password" .= "this is not a good password"]), 403) + ] + $ \(badPassword, stat) -> do + -- the status codes and error labels differ here, but the + -- important thing is that the request fails. + refreshAppCookie alice tid appId badPassword >>= assertStatus stat + + cookie' <- bindResponse (refreshAppCookie alice tid appId goodPassword) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "cookie" & asString - for_ [cookie, cookie'] $ \c -> - void $ bindResponse (renewToken OwnDomain c) $ \resp -> do - resp.status `shouldMatchInt` 200 - resp.json %. "user" `shouldMatch` appId - resp.json %. "token_type" `shouldMatch` "Bearer" - resp.json %. "access_token" & asString + renewToken OwnDomain cookie `bindResponse` \resp -> do + resp.status `shouldMatchInt` 403 + + renewToken OwnDomain cookie' `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "user" `shouldMatch` appId + resp.json %. "token_type" `shouldMatch` "Bearer" + void $ resp.json %. "access_token" & asString testDeleteAppFromTeam :: (HasCallStack) => App () testDeleteAppFromTeam = do @@ -228,14 +246,11 @@ testPutApp = do bindResponse (getApp owner tid appId) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json - `shouldMatchShape` SObject + `shouldMatchShapeLenient` SObject [ ("accent_id", SNumber), ("assets", SArray (SObject [("key", SString), ("size", SString), ("type", SString)])), ("name", SString), - ("category", SString), - ("description", SString), - ("metadata", SObject []), - ("picture", SArray SAny) + ("app", SObject [("category", SString), ("description", SString)]) ] let badAppId = "5e002eca-114f-11f1-b5a3-7306b8837f91" @@ -249,13 +264,9 @@ testRetrieveUsersIncludingApps = do [ ("accent_id", SNumber), ("assets", SArray SAny), ("id", SString), - ("locale", SString), - ("managed_by", SString), ("name", SString), - ("picture", SArray SAny), ("qualified_id", SObject [("domain", SString), ("id", SString)]), ("searchable", SBool), - ("status", SString), ("supported_protocols", SArray SString), ("team", SString), ("type", SString) @@ -270,17 +281,14 @@ testRetrieveUsersIncludingApps = do ] appShape = SObject - [ ("accent_id", SNumber), - ("assets", SArray SAny), - ("category", SString), - ("description", SString), - ("metadata", SObject []), - ("name", SString), - ("picture", SArray SAny) + [ ("category", SString), + ("description", SString) + ] + appWithIdShape = + SObject + [ ("id", SString), + ("app", appShape) ] - appWithIdShape = case appShape of - SObject attrs -> - SObject (("id", SString) : attrs) searchResultShape = SObject [ ("accent_id", SNumber), @@ -301,7 +309,7 @@ testRetrieveUsersIncludingApps = do resp.status `shouldMatchInt` 200 pure resp.json appCreated - `shouldMatchShape` SObject + `shouldMatchShapeLenient` SObject [ ("cookie", SString), ("user", userShape) ] @@ -324,25 +332,25 @@ testRetrieveUsersIncludingApps = do getTeamMember owner tid appId `bindResponse` \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "user" `shouldMatch` appId - resp.json `shouldMatchShape` memberShape + resp.json `shouldMatchShapeLenient` memberShape -- [`GET /teams/:tid/apps`](https://staging-nginz-https.zinfra.io/v15/api/swagger-ui/#/default/get-apps) (route id: "get-apps") getApps owner tid `bindResponse` \resp -> do resp.status `shouldMatchInt` 200 apps <- resp.json & maybe (error "this shouldn't happen") pure - apps `shouldMatchShape` SArray appWithIdShape + apps `shouldMatchShapeLenient` SArray appWithIdShape -- [`GET /teams/:tid/apps/:uid`](https://staging-nginz-https.zinfra.io/v15/api/swagger-ui/#/default/get-app) (route id: "get-app") getApp owner tid appId `bindResponse` \resp -> do resp.status `shouldMatchInt` 200 - resp.json `shouldMatchShape` appShape + resp.json `shouldMatchShapeLenient` userShape -- [`POST /list-users`](https://staging-nginz-https.zinfra.io/v15/api/swagger-ui/#/default/list-users-by-ids-or-handles) (route id: "list-users-by-ids-or-handles") listUsers owner [appCreated %. "user"] `bindResponse` \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "found.0" - `shouldMatchShape` SObject + `shouldMatchShapeLenient` SObject [ ("accent_id", SNumber), ("assets", SArray SAny), ("id", SString), @@ -367,4 +375,4 @@ testRetrieveUsersIncludingApps = do resp.status `shouldMatchInt` 200 hits :: [Value] <- resp.json %. "documents" & asList length hits `shouldMatchInt` 2 -- owner doesn't find itself - (`shouldMatchShape` searchResultShape) `mapM_` hits + (`shouldMatchShapeLenient` searchResultShape) `mapM_` hits diff --git a/integration/test/Test/Auth.hs b/integration/test/Test/Auth.hs index 9af1fb9dd78..a9f7ff7d451 100644 --- a/integration/test/Test/Auth.hs +++ b/integration/test/Test/Auth.hs @@ -252,7 +252,7 @@ testSetCookieLabelOnSsoFlow = do domain <- make OwnDomain (owner, tid, _) <- createTeam OwnDomain 1 void $ setTeamFeatureStatus owner tid "sso" "enabled" - void $ setTeamFeatureStatus owner tid "validateSAMLEmails" "enabled" + void $ setTeamFeatureStatus owner tid "validateSAMLemails" "enabled" idp@(samlId, _) <- do (resp, (meta, creds)) <- registerTestIdPWithMetaWithPrivateCreds owner resp.status `shouldMatchInt` 201 diff --git a/integration/test/Test/Events.hs b/integration/test/Test/Events.hs index d85b42ba326..a172cf2ff0f 100644 --- a/integration/test/Test/Events.hs +++ b/integration/test/Test/Events.hs @@ -55,8 +55,8 @@ import Testlib.ResourcePool import UnliftIO hiding (handle) testConsumeEventsOneWebSocket :: (HasCallStack) => App () -testConsumeEventsOneWebSocket = do - alice <- randomUser OwnDomain def +testConsumeEventsOneWebSocket = withModifiedBackend (enableConsumableNotifications def) $ \domain -> do + alice <- randomUser domain def lastNotifResp <- retrying @@ -95,11 +95,13 @@ testConsumeEventsOneWebSocket = do testWebSocketTimeout :: (HasCallStack) => App () testWebSocketTimeout = withModifiedBackend - def - { cannonCfg = - setField "wsOpts.activityTimeout" (1000000 :: Int) - >=> setField "wsOpts.pongTimeout" (1000000 :: Int) - } + ( enableConsumableNotifications + def + { cannonCfg = + setField "wsOpts.activityTimeout" (1000000 :: Int) + >=> setField "wsOpts.pongTimeout" (1000000 :: Int) + } + ) $ \domain -> do alice <- randomUser domain def client <- addClient alice def {acapabilities = Just ["consumable-notifications"]} >>= getJSON 201 @@ -118,8 +120,8 @@ testWebSocketTimeout = withModifiedBackend $ assertFailure "Expected web socket timeout" testConsumeTempEvents :: (HasCallStack) => App () -testConsumeTempEvents = do - alice <- randomUser OwnDomain def +testConsumeTempEvents = withModifiedBackend (enableConsumableNotifications def) $ \domain -> do + alice <- randomUser domain def client0 <- addClient alice def {acapabilities = Just ["consumable-notifications"]} >>= getJSON 201 clientId0 <- objId client0 @@ -160,7 +162,7 @@ testConsumeTempEvents = do testTemporaryQueuesAreDeletedAfterUse :: (HasCallStack) => App () testTemporaryQueuesAreDeletedAfterUse = do - startDynamicBackendsReturnResources [def] $ \[beResource] -> do + startDynamicBackendsReturnResources [enableConsumableNotifications def] $ \[beResource] -> do let domain = beResource.berDomain rabbitmqAdmin <- mkRabbitMqAdminClientForResource beResource [alice, bob] <- createAndConnectUsers [domain, domain] @@ -204,8 +206,8 @@ testTemporaryQueuesAreDeletedAfterUse = do queuesAfterWS.items `shouldMatchSet` [deadNotifsQueue, cellsEventsQueue, aliceClientQueue, backgroundJobsQueue] testSendMessageNoReturnToSenderWithConsumableNotificationsProteus :: (HasCallStack) => App () -testSendMessageNoReturnToSenderWithConsumableNotificationsProteus = do - (alice, tid, bob : _) <- createTeam OwnDomain 2 +testSendMessageNoReturnToSenderWithConsumableNotificationsProteus = withModifiedBackend (enableConsumableNotifications def) $ \domain -> do + (alice, tid, bob : _) <- createTeam domain 2 aliceOldClient <- addClient alice def >>= getJSON 201 aliceClient <- addClient alice def {acapabilities = Just ["consumable-notifications"]} >>= getJSON 201 aliceClientId <- objId aliceClient @@ -237,8 +239,8 @@ testSendMessageNoReturnToSenderWithConsumableNotificationsProteus = do assertNoEvent_ ws testEventsForSpecificClients :: (HasCallStack) => App () -testEventsForSpecificClients = do - alice <- randomUser OwnDomain def +testEventsForSpecificClients = withModifiedBackend (enableConsumableNotifications def) $ \domain -> do + alice <- randomUser domain def uid <- objId alice client1 <- addClient alice def {acapabilities = Just ["consumable-notifications"]} >>= getJSON 201 cid1 <- objId client1 @@ -262,7 +264,7 @@ testEventsForSpecificClients = do "payload" .= [object ["hello" .= "client2"]] ] - GundeckInternal.postPush OwnDomain [eventForClient1, eventForClient2] >>= assertSuccess + GundeckInternal.postPush domain [eventForClient1, eventForClient2] >>= assertSuccess assertEvent ws1 $ \e -> e %. "data.event.payload.0.hello" `shouldMatch` "client1" @@ -273,9 +275,9 @@ testEventsForSpecificClients = do $ assertNoEvent_ wsTemp testConsumeEventsForDifferentUsers :: (HasCallStack) => App () -testConsumeEventsForDifferentUsers = do - alice <- randomUser OwnDomain def - bob <- randomUser OwnDomain def +testConsumeEventsForDifferentUsers = withModifiedBackend (enableConsumableNotifications def) $ \domain -> do + alice <- randomUser domain def + bob <- randomUser domain def aliceClient <- addClient alice def {acapabilities = Just ["consumable-notifications"]} >>= getJSON 201 aliceClientId <- objId aliceClient @@ -299,8 +301,8 @@ testConsumeEventsForDifferentUsers = do sendAck ws deliveryTag False testConsumeEventsWhileHavingLegacyClients :: (HasCallStack) => App () -testConsumeEventsWhileHavingLegacyClients = do - alice <- randomUser OwnDomain def +testConsumeEventsWhileHavingLegacyClients = withModifiedBackend (enableConsumableNotifications def) $ \domain -> do + alice <- randomUser domain def -- Even if alice has no clients, the notifications should still be persisted -- in Cassandra. This choice is kinda arbitrary as these notifications @@ -333,8 +335,8 @@ testConsumeEventsWhileHavingLegacyClients = do resp.json %. "notifications.1.payload.0.type" `shouldMatch` "user.client-add" testConsumeEventsAcks :: (HasCallStack) => App () -testConsumeEventsAcks = do - alice <- randomUser OwnDomain def +testConsumeEventsAcks = withModifiedBackend (enableConsumableNotifications def) $ \domain -> do + alice <- randomUser domain def client <- addClient alice def {acapabilities = Just ["consumable-notifications"]} >>= getJSON 201 clientId <- objId client @@ -355,8 +357,8 @@ testConsumeEventsAcks = do assertNoEvent_ ws testConsumeEventsMultipleAcks :: (HasCallStack) => App () -testConsumeEventsMultipleAcks = do - alice <- randomUser OwnDomain def +testConsumeEventsMultipleAcks = withModifiedBackend (enableConsumableNotifications def) $ \domain -> do + alice <- randomUser domain def client <- addClient alice def {acapabilities = Just ["consumable-notifications"]} >>= getJSON 201 clientId <- objId client @@ -379,8 +381,8 @@ testConsumeEventsMultipleAcks = do assertNoEvent_ ws testConsumeEventsAckNewEventWithoutAckingOldOne :: (HasCallStack) => App () -testConsumeEventsAckNewEventWithoutAckingOldOne = do - alice <- randomUser OwnDomain def +testConsumeEventsAckNewEventWithoutAckingOldOne = withModifiedBackend (enableConsumableNotifications def) $ \domain -> do + alice <- randomUser domain def client <- addClient alice def {acapabilities = Just ["consumable-notifications"]} >>= getJSON 201 clientId <- objId client @@ -415,7 +417,7 @@ testConsumeEventsAckNewEventWithoutAckingOldOne = do testEventsDeadLettered :: (HasCallStack) => App () testEventsDeadLettered = do let notifTTL = 1 # Second - withModifiedBackend (def {gundeckCfg = setField "settings.notificationTTL" (notifTTL #> Second)}) $ \domain -> do + withModifiedBackend (enableConsumableNotifications (def {gundeckCfg = setField "settings.notificationTTL" (notifTTL #> Second)})) $ \domain -> do alice <- randomUser domain def -- This generates an event @@ -449,7 +451,7 @@ testEventsDeadLettered = do testEventsDeadLetteredWithReconnect :: (HasCallStack) => App () testEventsDeadLetteredWithReconnect = do let notifTTL = 1 # Second - startDynamicBackendsReturnResources [def {gundeckCfg = setField "settings.notificationTTL" (notifTTL #> Second)}] $ \[resources] -> do + startDynamicBackendsReturnResources [enableConsumableNotifications (def {gundeckCfg = setField "settings.notificationTTL" (notifTTL #> Second)})] $ \[resources] -> do let domain :: String = resources.berDomain alice <- randomUser domain def @@ -501,7 +503,7 @@ testEventsDeadLetteredWithReconnect = do testTransientEventsDoNotTriggerDeadLetters :: (HasCallStack) => App () testTransientEventsDoNotTriggerDeadLetters = do let notifTTL = 1 # Second - withModifiedBackend (def {gundeckCfg = setField "settings.notificationTTL" (notifTTL #> Second)}) $ \domain -> do + withModifiedBackend (enableConsumableNotifications (def {gundeckCfg = setField "settings.notificationTTL" (notifTTL #> Second)})) $ \domain -> do alice <- randomUser domain def -- Creates a non-transient event client <- addClient alice def {acapabilities = Just ["consumable-notifications"]} >>= getJSON 201 @@ -527,9 +529,9 @@ testTransientEventsDoNotTriggerDeadLetters = do assertNoEvent_ ws testTransientEvents :: (HasCallStack) => App () -testTransientEvents = do - (alice, _, _) <- mkUserPlusClient - (bob, _, bobClient) <- mkUserPlusClient +testTransientEvents = withModifiedBackend (enableConsumableNotifications def) $ \domain -> do + (alice, _, _) <- mkUserPlusClientWithDomain domain + (bob, _, bobClient) <- mkUserPlusClientWithDomain domain connectTwoUsers alice bob bobClientId <- objId bobClient @@ -567,11 +569,13 @@ testTransientEvents = do testChannelLimit :: (HasCallStack) => App () testChannelLimit = withModifiedBackend - ( def - { cannonCfg = - setField "rabbitMqMaxChannels" (2 :: Int) - >=> setField "rabbitMqMaxConnections" (1 :: Int) - } + ( enableConsumableNotifications + ( def + { cannonCfg = + setField "rabbitMqMaxChannels" (2 :: Int) + >=> setField "rabbitMqMaxConnections" (1 :: Int) + } + ) ) $ \domain -> do alice <- randomUser domain def @@ -609,7 +613,7 @@ testChannelKilled = do void $ killAllRabbitMqConns backend waitUntilNoRabbitMqConns backend - runCodensity (startDynamicBackend backend def) $ \_ -> do + runCodensity (startDynamicBackend backend (enableConsumableNotifications def)) $ \_ -> do let domain = backend.berDomain alice <- randomUser domain def [c1, c2] <- @@ -643,8 +647,8 @@ testChannelKilled = do assertNoEventHelper ws `shouldMatch` WebSocketDied testSingleConsumer :: (HasCallStack) => App () -testSingleConsumer = do - alice <- randomUser OwnDomain def +testSingleConsumer = withModifiedBackend (enableConsumableNotifications def) $ \domain -> do + alice <- randomUser domain def clientId <- addClient alice def {acapabilities = Just ["consumable-notifications"]} >>= getJSON 201 @@ -682,8 +686,8 @@ testSingleConsumer = do lift $ assertNoEvent_ ws' testPrefetchCount :: (HasCallStack) => App () -testPrefetchCount = do - (alice, uid, cid) <- mkUserPlusClient +testPrefetchCount = withModifiedBackend (enableConsumableNotifications def) $ \domain -> do + (alice, uid, cid) <- mkUserPlusClientWithDomain domain emptyQueue alice cid for_ [1 :: Int .. 550] $ \i -> @@ -693,7 +697,7 @@ testPrefetchCount = do [ "recipients" .= [object ["user_id" .= uid, "clients" .= [cid], "route" .= "any"]], "payload" .= [object ["no" .= show i]] ] - GundeckInternal.postPush OwnDomain [event] >>= assertSuccess + GundeckInternal.postPush domain [event] >>= assertSuccess runCodensity (createEventsWebSocketWithSync alice (Just cid)) \(endMarker, ws) -> do es <- consumeAllEventsNoAck ws assertBool ("First 500 events expected, got " ++ show (length es)) $ length es == 500 @@ -704,11 +708,11 @@ testPrefetchCount = do assertBool "Receive at least one outstanding event" $ not (null es') testEndOfInitialSync :: (HasCallStack) => App () -testEndOfInitialSync = do - (alice, uid, cid) <- mkUserPlusClient +testEndOfInitialSync = withModifiedBackend (enableConsumableNotifications def) $ \domain -> do + (alice, uid, cid) <- mkUserPlusClientWithDomain domain let n = 20 replicateM_ n $ do - GundeckInternal.postPush OwnDomain [mkEvent uid cid False] >>= assertSuccess + GundeckInternal.postPush domain [mkEvent uid cid False] >>= assertSuccess -- marker0 <- randomId runCodensity (createEventsWebSocketWithSync alice (Just cid)) \(endMarker, ws) -> do @@ -719,7 +723,7 @@ testEndOfInitialSync = do length (preExistingEvents <> otherEvents) `shouldMatchInt` (n + 2) -- more events should not be followed by the sync event - GundeckInternal.postPush OwnDomain [mkEvent uid cid False] >>= assertSuccess + GundeckInternal.postPush domain [mkEvent uid cid False] >>= assertSuccess assertEvent ws $ \e -> do e %. "data.event.payload.0.type" `shouldMatch` "test" ackEvent ws e @@ -734,18 +738,18 @@ testEndOfInitialSync = do length events `shouldMatchInt` 1 -- more events should not be followed by synchronization event - GundeckInternal.postPush OwnDomain [mkEvent uid cid False] >>= assertSuccess + GundeckInternal.postPush domain [mkEvent uid cid False] >>= assertSuccess assertEvent ws $ \e -> do e %. "data.event.payload.0.type" `shouldMatch` "test" ackEvent ws e assertNoEvent_ ws testEndOfInitialSyncMoreEventsAfterSyncMessage :: (HasCallStack) => App () -testEndOfInitialSyncMoreEventsAfterSyncMessage = do - (alice, uid, cid) <- mkUserPlusClient +testEndOfInitialSyncMoreEventsAfterSyncMessage = withModifiedBackend (enableConsumableNotifications def) $ \domain -> do + (alice, uid, cid) <- mkUserPlusClientWithDomain domain let n = 20 replicateM_ n $ do - GundeckInternal.postPush OwnDomain [mkEvent uid cid False] >>= assertSuccess + GundeckInternal.postPush domain [mkEvent uid cid False] >>= assertSuccess runCodensity (createEventsWebSocketWithSync alice (Just cid)) \(endMarker, ws) -> do -- it seems this is needed to reduce flakiness, @@ -754,7 +758,7 @@ testEndOfInitialSyncMoreEventsAfterSyncMessage = do -- before consuming, we push n more events replicateM_ n $ do - GundeckInternal.postPush OwnDomain [mkEvent uid cid False] >>= assertSuccess + GundeckInternal.postPush domain [mkEvent uid cid False] >>= assertSuccess preExistingEvents <- consumeEventsUntilEndOfInitialSync ws endMarker otherEvents <- consumeAllEvents ws @@ -765,14 +769,14 @@ testEndOfInitialSyncMoreEventsAfterSyncMessage = do `shouldMatch` True testEndOfInitialSyncIgnoreExpired :: (HasCallStack) => App () -testEndOfInitialSyncIgnoreExpired = do - (alice, uid, cid) <- mkUserPlusClient +testEndOfInitialSyncIgnoreExpired = withModifiedBackend (enableConsumableNotifications def) $ \domain -> do + (alice, uid, cid) <- mkUserPlusClientWithDomain domain let n = 20 replicateM_ n $ do - GundeckInternal.postPush OwnDomain [mkEvent uid cid False] >>= assertSuccess + GundeckInternal.postPush domain [mkEvent uid cid False] >>= assertSuccess replicateM_ n $ do - GundeckInternal.postPush OwnDomain [mkEvent uid cid True] >>= assertSuccess + GundeckInternal.postPush domain [mkEvent uid cid True] >>= assertSuccess -- Wait for transient events to expire Timeout.threadDelay (1 # Second) @@ -784,11 +788,11 @@ testEndOfInitialSyncIgnoreExpired = do length events `shouldMatchInt` (n + 2) -- +1 for the sync event, +1 for the client add event testEndOfInitialSyncAckMultiple :: (HasCallStack) => App () -testEndOfInitialSyncAckMultiple = do - (alice, uid, cid) <- mkUserPlusClient +testEndOfInitialSyncAckMultiple = withModifiedBackend (enableConsumableNotifications def) $ \domain -> do + (alice, uid, cid) <- mkUserPlusClientWithDomain domain let n = 20 replicateM_ n $ do - GundeckInternal.postPush OwnDomain [mkEvent uid cid False] >>= assertSuccess + GundeckInternal.postPush domain [mkEvent uid cid False] >>= assertSuccess runCodensity (createEventsWebSocketWithSync alice (Just cid)) $ \(endMarker, ws) -> do void $ assertEvent ws pure @@ -812,43 +816,51 @@ mkEvent uid cid transient = testTypingIndicatorIsNotSentToOwnClient :: (HasCallStack) => TaggedBool "federated" -> App () testTypingIndicatorIsNotSentToOwnClient (TaggedBool federated) = do - (alice, _, aliceClient) <- mkUserPlusClientWithDomain OwnDomain - (bob, _, bobClient) <- mkUserPlusClientWithDomain (if federated then OtherDomain else OwnDomain) - connectTwoUsers alice bob - aliceClientId <- objId aliceClient - bobClientId <- objId bobClient - conv <- postConversation alice defProteus {qualifiedUsers = [bob]} >>= getJSON 201 - - runCodensity (createEventWebSockets [(alice, Just aliceClientId), (bob, Just bobClientId)]) $ \[aliceWs, bobWs] -> do - -- consume all events to ensure we start with a clean slate - consumeAllEvents_ aliceWs - consumeAllEvents_ bobWs - - -- Alice is typing - sendTypingStatus alice conv "started" >>= assertSuccess - - -- Bob should receive the typing indicator for Alice - assertEvent bobWs $ \e -> do - e %. "data.event.payload.0.type" `shouldMatch` "conversation.typing" - e %. "data.event.payload.0.qualified_conversation" `shouldMatch` (conv %. "qualified_id") - e %. "data.event.payload.0.qualified_from" `shouldMatch` (alice %. "qualified_id") - ackEvent bobWs e - - -- Alice should not receive the typing indicator for herself - assertNoEvent_ aliceWs - - -- Bob is typing - sendTypingStatus bob conv "started" >>= assertSuccess - - -- Alice should receive the typing indicator for Bob - assertEvent aliceWs $ \e -> do - e %. "data.event.payload.0.type" `shouldMatch` "conversation.typing" - e %. "data.event.payload.0.qualified_conversation" `shouldMatch` (conv %. "qualified_id") - e %. "data.event.payload.0.qualified_from" `shouldMatch` (bob %. "qualified_id") - ackEvent aliceWs e - - -- Bob should not receive the typing indicator for himself - assertNoEvent_ bobWs + let runTest = + if federated + then startDynamicBackends [(enableConsumableNotifications def), (enableConsumableNotifications def)] + else \run -> startDynamicBackends [(enableConsumableNotifications def)] $ \[domain] -> run [domain, domain] + runTest $ \[domain, otherDomain] -> do + (alice, _, aliceClient) <- mkUserPlusClientWithDomain domain + (bob, _, bobClient) <- mkUserPlusClientWithDomain otherDomain + connectTwoUsers alice bob + aliceClientId <- objId aliceClient + bobClientId <- objId bobClient + conv <- postConversation alice defProteus {qualifiedUsers = [bob]} >>= getJSON 201 + + runCodensity (createEventWebSockets [(alice, Just aliceClientId), (bob, Just bobClientId)]) $ \[aliceWs, bobWs] -> do + -- consume all events to ensure we start with a clean slate + consumeAllEvents_ aliceWs + consumeAllEvents_ bobWs + + -- Alice is typing + sendTypingStatus alice conv "started" >>= assertSuccess + + -- Bob should receive the typing indicator for Alice + assertEvent bobWs $ \e -> do + e %. "data.event.payload.0.type" `shouldMatch` "conversation.typing" + e %. "data.event.payload.0.qualified_conversation" `shouldMatch` (conv %. "qualified_id") + e %. "data.event.payload.0.qualified_from" `shouldMatch` (alice %. "qualified_id") + ackEvent bobWs e + + -- Alice should not receive the typing indicator for herself + assertNoEvent_ aliceWs + + -- Bob is typing + sendTypingStatus bob conv "started" >>= assertSuccess + + -- Alice should receive the typing indicator for Bob + assertEvent aliceWs $ \e -> do + e %. "data.event.payload.0.type" `shouldMatch` "conversation.typing" + e %. "data.event.payload.0.qualified_conversation" `shouldMatch` (conv %. "qualified_id") + e %. "data.event.payload.0.qualified_from" `shouldMatch` (bob %. "qualified_id") + ackEvent aliceWs e + + -- Bob should not receive the typing indicator for himself + assertNoEvent_ bobWs + +-- convert :: ((HasCallStack) => (String -> App ()) -> App ()) -> ([String] -> App ()) -> App () +-- convert = undefined -- We only delete queues to clean up federated integration tests. So, we -- mostly want to ensure we don't get stuck there. @@ -860,12 +872,14 @@ testBackendPusherRecoversFromQueueDeletion = do let remotesRefreshInterval = 10000 :: Int startDynamicBackendsReturnResources - [ def - { backgroundWorkerCfg = - setField - "backendNotificationPusher.remotesRefreshInterval" - remotesRefreshInterval - } + [ enableConsumableNotifications + ( def + { backgroundWorkerCfg = + setField + "backendNotificationPusher.remotesRefreshInterval" + remotesRefreshInterval + } + ) ] $ \[beResource] -> do let domain = beResource.berDomain @@ -1243,3 +1257,11 @@ mkRabbitMqAdminClientForResource backend = do opts <- asks (.rabbitMQConfig) servantClient <- liftIO $ mkRabbitMqAdminClientEnv opts {vHost = Text.pack backend.berVHost} pure . fromServant $ Servant.hoistClient (Proxy @(ToServant AdminAPI AsApi)) (liftIO @App) (toServant servantClient) + +enableConsumableNotifications :: ServiceOverrides -> ServiceOverrides +enableConsumableNotifications overrides = + overrides + <> def + { brigCfg = setField "optSettings.setConsumableNotifications" True, + gundeckCfg = setField "settings.consumableNotifications" True + } diff --git a/integration/test/Test/FeatureFlags/ValidateSAMLEmails.hs b/integration/test/Test/FeatureFlags/RequireExternalEmailVerification.hs similarity index 80% rename from integration/test/Test/FeatureFlags/ValidateSAMLEmails.hs rename to integration/test/Test/FeatureFlags/RequireExternalEmailVerification.hs index 9b4f581b873..5176382afcb 100644 --- a/integration/test/Test/FeatureFlags/ValidateSAMLEmails.hs +++ b/integration/test/Test/FeatureFlags/RequireExternalEmailVerification.hs @@ -15,19 +15,19 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Test.FeatureFlags.ValidateSAMLEmails where +module Test.FeatureFlags.RequireExternalEmailVerification where import SetupHelpers import Test.FeatureFlags.Util import Testlib.Prelude -testPatchValidateSAMLEmails :: (HasCallStack) => App () -testPatchValidateSAMLEmails = +testPatchRequireExternalEmailVerification :: (HasCallStack) => App () +testPatchRequireExternalEmailVerification = checkPatch OwnDomain "validateSAMLemails" $ object ["status" .= "disabled"] -testValidateSAMLEmailsInternal :: (HasCallStack) => App () -testValidateSAMLEmailsInternal = do +testRequireExternalEmailVerification :: (HasCallStack) => App () +testRequireExternalEmailVerification = do (alice, tid, _) <- createTeam OwnDomain 0 withWebSocket alice $ \ws -> do setFlag InternalAPI ws tid "validateSAMLemails" disabled diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index 51fd2809951..de871aae26e 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -1094,6 +1094,9 @@ testGroupInfoMismatch = do length clients `shouldMatchInt` 3 resp.json %. "commit" `shouldMatchBase64` mp2.message resp.json %. "group_info" `shouldMatchBase64` (fromJust mp1.groupInfo) + resp.json %. "code" `shouldMatchInt` 400 + resp.json %. "label" `shouldMatch` "inconsistent-group-state" + resp.json %. "message" `shouldMatch` "Submitted group info is inconsistent with the backend group state" -- check that epoch is still 1 bindResponse (getConversation alice convId) $ \resp -> do @@ -1114,6 +1117,9 @@ testGroupInfoMismatch = do length clients `shouldMatchInt` 3 resp.json %. "commit" `shouldMatchBase64` mp3.message resp.json %. "group_info" `shouldMatchBase64` (fromJust mp1.groupInfo) + resp.json %. "code" `shouldMatchInt` 400 + resp.json %. "label" `shouldMatch` "inconsistent-group-state" + resp.json %. "message" `shouldMatch` "Submitted group info is inconsistent with the backend group state" -- check that epoch is still 1 bindResponse (getConversation alice convId) $ \resp -> do @@ -1183,3 +1189,23 @@ testAddUsersDirectlyShouldFail = do addMembers alice conv def {users = [bob]} `bindResponse` \resp -> do resp.status `shouldMatchInt` 403 resp.json %. "label" `shouldMatch` "invalid-op" + +testGroupIdParseError :: (HasCallStack) => App () +testGroupIdParseError = do + [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] + [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] + void $ uploadNewKeyPackage def bob1 + conv <- postConversation alice1 defMLS >>= getJSON 201 + convId0 <- objConvId conv + + -- break group ID + let convId = convId0 {groupId = fmap (\gid -> "k" <> tail gid) convId0.groupId} :: ConvId + + createGroup def alice1 convId + + mp <- createAddCommit alice1 convId [alice, bob] + bindResponse (postMLSCommitBundle mp.sender (mkBundle mp)) $ \resp -> do + resp.status `shouldMatchInt` 400 + resp.json %. "label" `shouldMatch` "mls-protocol-error" + msg <- resp.json %. "message" & asString + assertBool "unexpected error message" $ "Could not parse group ID:" `isPrefixOf` msg diff --git a/integration/test/Test/MLS/KeyPackage.hs b/integration/test/Test/MLS/KeyPackage.hs index b73ec27f113..2ca5d2c4c00 100644 --- a/integration/test/Test/MLS/KeyPackage.hs +++ b/integration/test/Test/MLS/KeyPackage.hs @@ -25,6 +25,15 @@ testDeleteKeyPackages = do resp.status `shouldMatchInt` 200 resp.json %. "count" `shouldMatchInt` 0 +testClaimKeyPackagesUserDeleted :: App () +testClaimKeyPackagesUserDeleted = do + (_, _, [alice]) <- createTeam OwnDomain 2 + alice1 <- createMLSClient def alice + API.Brig.deleteUser alice >>= assertSuccess + bindResponse (claimKeyPackages def alice1 alice) $ \resp -> do + resp.status `shouldMatchInt` 400 + resp.json %. "label" `shouldMatch` "invalid-user" + testKeyPackageMultipleCiphersuites :: App () testKeyPackageMultipleCiphersuites = do let suite = def diff --git a/integration/test/Test/Meetings.hs b/integration/test/Test/Meetings.hs index 1fce53ff225..f63ca75e937 100644 --- a/integration/test/Test/Meetings.hs +++ b/integration/test/Test/Meetings.hs @@ -197,3 +197,117 @@ testMeetingUpdateUnauthorized = do ] putMeeting otherUser domain meetingId update >>= assertStatus 404 + +testMeetingListEmpty :: (HasCallStack) => App () +testMeetingListEmpty = do + (owner, _tid, _members) <- createTeam OwnDomain 1 + resp <- getMeetingsList owner + assertSuccess resp + meetings <- resp.json & asList + length (meetings :: [Value]) `shouldMatchInt` 0 + +testMeetingListNoMeetings :: (HasCallStack) => App () +testMeetingListNoMeetings = do + (owner, _tid, _members) <- createTeam OwnDomain 1 + _ <- createTeam OwnDomain 1 + resp <- getMeetingsList owner + assertSuccess resp + meetings <- resp.json & asList + length (meetings :: [Value]) `shouldMatchInt` 0 + +testMeetingListMultiple :: (HasCallStack) => App () +testMeetingListMultiple = do + (owner, _tid, _members) <- createTeam OwnDomain 1 + now <- liftIO getCurrentTime + let firstMeeting = defaultMeetingJson "First Meeting" (addUTCTime 3600 now) (addUTCTime 7200 now) [] + secondMeeting = defaultMeetingJson "Second Meeting" (addUTCTime 3600 now) (addUTCTime 7200 now) [] + thirdMeeting = defaultMeetingJson "Third Meeting" (addUTCTime 3600 now) (addUTCTime 7200 now) [] + r1 <- postMeetings owner firstMeeting + assertSuccess r1 + m1 <- getJSON 201 r1 + (id1, _) <- getMeetingIdAndDomain m1 + + r2 <- postMeetings owner secondMeeting + assertSuccess r2 + m2 <- getJSON 201 r2 + (id2, _) <- getMeetingIdAndDomain m2 + + r3 <- postMeetings owner thirdMeeting + assertSuccess r3 + m3 <- getJSON 201 r3 + (id3, _) <- getMeetingIdAndDomain m3 + + resp <- getMeetingsList owner + assertSuccess resp + meetings <- resp.json & asList + length (meetings :: [Value]) `shouldMatchInt` 3 + + titles <- forM meetings $ \m -> m %. "title" >>= asString + let expectedTitles = ["First Meeting", "Second Meeting", "Third Meeting"] + (all (`elem` titles) expectedTitles) `shouldMatch` True + + fetchedIds <- forM meetings $ \m -> m %. "qualified_id" %. "id" >>= asString + let expectedIds = [id1, id2, id3] + (all (`elem` fetchedIds) expectedIds) `shouldMatch` True + +testMeetingListPagination :: (HasCallStack) => App () +testMeetingListPagination = do + (owner, _tid, _members) <- createTeam OwnDomain 1 + now <- liftIO getCurrentTime + + -- The internal page size is 1000, so we create 1001 meetings to test pagination. + -- This ensures `hasMore = True` is triggered and multiple pages are fetched. + forM_ [(1 :: Int) .. 1001] $ \i -> do + let meeting = defaultMeetingJson ("Meeting " <> show i) (addUTCTime 3600 now) (addUTCTime 7200 now) [] + postMeetings owner meeting >>= assertStatus 201 + + resp <- getMeetingsList owner + assertSuccess resp + meetings <- resp.json & asList + length (meetings :: [Value]) `shouldMatchInt` 1001 + +testMeetingAddInvitation :: (HasCallStack) => App () +testMeetingAddInvitation = do + (owner, _tid, _members) <- createTeam OwnDomain 1 + now <- liftIO getCurrentTime + let startTime = addUTCTime 3600 now + endTime = addUTCTime 7200 now + newMeeting = defaultMeetingJson "Team Standup" startTime endTime ["alice@example.com"] + meeting <- postMeetings owner newMeeting >>= getJSON 201 + (meetingId, domain) <- getMeetingIdAndDomain meeting + let invitation = object ["emails" .= ["bob@example.com"]] + postMeetingInvitation owner domain meetingId invitation >>= assertStatus 200 + updated <- getMeeting owner domain meetingId >>= getJSON 200 + updated %. "invited_emails" `shouldMatch` ["alice@example.com", "bob@example.com"] + +testMeetingAddInvitationNotFound :: (HasCallStack) => App () +testMeetingAddInvitationNotFound = do + (owner, _tid, _members) <- createTeam OwnDomain 1 + fakeMeetingId <- randomId + let invitation = object ["emails" .= ["bob@example.com"]] + postMeetingInvitation owner "example.com" fakeMeetingId invitation >>= assertStatus 404 + +testMeetingRemoveInvitation :: (HasCallStack) => App () +testMeetingRemoveInvitation = do + (owner, _tid, _members) <- createTeam OwnDomain 1 + now <- liftIO getCurrentTime + let startTime = addUTCTime 3600 now + endTime = addUTCTime 7200 now + newMeeting = defaultMeetingJson "Team Standup" startTime endTime ["alice@example.com", "bob@example.com"] + + meeting <- postMeetings owner newMeeting >>= getJSON 201 + (meetingId, domain) <- getMeetingIdAndDomain meeting + let removeInvitation = object ["emails" .= ["alice@example.com"]] + + deleteMeetingInvitation owner domain meetingId removeInvitation >>= assertStatus 200 + + updated <- getMeeting owner domain meetingId >>= getJSON 200 + updated %. "invited_emails" `shouldMatch` ["bob@example.com"] + +testMeetingRemoveInvitationNotFound :: (HasCallStack) => App () +testMeetingRemoveInvitationNotFound = do + (owner, _tid, _members) <- createTeam OwnDomain 1 + fakeMeetingId <- randomId + let removeInvitation = object ["emails" .= ["alice@example.com"]] + + deleteMeetingInvitation owner "example.com" fakeMeetingId removeInvitation >>= assertStatus 404 diff --git a/integration/test/Test/Search.hs b/integration/test/Test/Search.hs index 33eed64e314..b91fd0523d6 100644 --- a/integration/test/Test/Search.hs +++ b/integration/test/Test/Search.hs @@ -32,8 +32,7 @@ import SetupHelpers import Testlib.Assertions import Testlib.Prelude --------------------------------------------------------------------------------- --- LOCAL SEARCH +-- * Local Search testSearchContactForExternalUsers :: (HasCallStack) => App () testSearchContactForExternalUsers = do @@ -96,8 +95,7 @@ testEphemeralUsersSearch = do resp.status `shouldMatchInt` 403 resp.json %. "label" `shouldMatch` "insufficient-permissions" --------------------------------------------------------------------------------- --- FEDERATION SEARCH +-- * Federation Search -- | Enumeration of the possible restrictions that can be applied to a federated user search data Restriction = AllowAll | TeamAllowed | TeamNotAllowed @@ -305,8 +303,7 @@ testFederatedUserSearchForNonTeamUser = do doc : _ -> assertFailure $ "Expected an empty result, but got " <> show doc <> " for test case " --------------------------------------------------------------------------------- --- TEAM SEARCH +-- * Team Search testSearchForTeamMembersWithRoles :: (HasCallStack) => App () testSearchForTeamMembersWithRoles = do @@ -425,6 +422,8 @@ testTeamSearchUserIncludesUserGroups = do actualUgs <- for ugs asString actualUgs `shouldMatchSet` expectedUgs +-- * Contacts Search + testUserSearchable :: App () testUserSearchable = do -- Create team and all users who are part of this test @@ -543,3 +542,50 @@ testUserSearchable = do resp.status `shouldMatchInt` 200 docs <- resp.json %. "documents" >>= asList f docs + +testSuspendedUserSearch :: (HasCallStack) => App () +testSuspendedUserSearch = do + [searcher, searchee] <- replicateM 2 $ randomUser OwnDomain def + BrigI.refreshIndex OwnDomain + searcheeQid <- objQidObject searchee + + -- The searcher can find the searchee by default + assertCanFind searcher searcheeQid (searchee %. "name") OwnDomain + + -- The searcher cannot find the searchee because the searchee is suspended + BrigI.setAccountStatus searchee "suspended" >>= assertSuccess + BrigI.getAccountStatus searchee `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "status" `shouldMatch` "suspended" + BrigI.refreshIndex OwnDomain + assertCannotFind searcher searcheeQid (searchee %. "name") OwnDomain + + -- The searcher can find the searchee once the searchee is unsuspended + BrigI.setAccountStatus searchee "active" >>= assertSuccess + BrigI.getAccountStatus searchee `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "status" `shouldMatch` "active" + BrigI.refreshIndex OwnDomain + assertCanFind searcher searcheeQid (searchee %. "name") OwnDomain + +-- * Assertion Helpers + +assertCanFind :: + (HasCallStack, MakesValue searcher, MakesValue domain, MakesValue searcheeQid, MakesValue searchTerm) => + searcher -> searcheeQid -> searchTerm -> domain -> App () +assertCanFind searcher searcheeQid q domain = + BrigP.searchContacts searcher q domain `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + foundDocs :: [Value] <- resp.json %. "documents" >>= asList + foundIds <- objQidObject `mapM` foundDocs + searcheeQid `shouldMatchOneOf` foundIds + +assertCannotFind :: + (HasCallStack, MakesValue searcher, MakesValue domain, MakesValue searcheeQid, MakesValue searchTerm) => + searcher -> searcheeQid -> searchTerm -> domain -> App () +assertCannotFind searcher searcheeQid q domain = + BrigP.searchContacts searcher q domain `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + foundDocs :: [Value] <- resp.json %. "documents" >>= asList + foundIds <- objQid `mapM` foundDocs + searcheeQid `shouldNotMatchOneOf` foundIds diff --git a/integration/test/Test/Shape.hs b/integration/test/Test/Shape.hs index eeebf5fe438..64761b5f222 100644 --- a/integration/test/Test/Shape.hs +++ b/integration/test/Test/Shape.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 . --- | Self-tests for the 'Shape' DSL and 'shouldMatchShape' assertion. +-- | Self-tests for the 'Shape' DSL and 'shouldMatchShape*' assertions. module Test.Shape where import Testlib.Prelude @@ -26,35 +26,41 @@ import Testlib.Prelude testShapeObjectMatch :: (HasCallStack) => App () testShapeObjectMatch = do let v = object ["foo" .= (42 :: Int), "bar" .= ("hello" :: String)] - v `shouldMatchShape` SObject [("foo", SNumber), ("bar", SString)] + v `shouldMatchShapeExact` SObject [("foo", SNumber), ("bar", SString)] + +-- | A matching object shape succeeds. +testShapeObjectMatchLenient :: (HasCallStack) => App () +testShapeObjectMatchLenient = do + let v = object ["foo" .= (42 :: Int), "bar" .= ("hello" :: String)] + v `shouldMatchShapeLenient` SObject [("foo", SNumber)] -- | An unexpected key in the actual object causes a failure. testShapeUnexpectedKey :: (HasCallStack) => App () testShapeUnexpectedKey = do let v = object ["foo" .= (1 :: Int), "extra" .= (2 :: Int)] expectFailure (\_ -> pure ()) do - v `shouldMatchShape` SObject [("foo", SNumber)] + v `shouldMatchShapeExact` SObject [("foo", SNumber)] -- | A missing key in the actual object causes a failure. testShapeMissingKey :: (HasCallStack) => App () testShapeMissingKey = do let v = object ["foo" .= (1 :: Int)] expectFailure (\_ -> pure ()) do - v `shouldMatchShape` SObject [("foo", SNumber), ("bar", SString)] + v `shouldMatchShapeExact` SObject [("foo", SNumber), ("bar", SString)] -- | Providing a non-object value when 'SObject' is expected causes a failure. testShapeWrongTypeObject :: (HasCallStack) => App () testShapeWrongTypeObject = do let v = toJSON ("hello" :: String) expectFailure (\_ -> pure ()) do - v `shouldMatchShape` SObject [("foo", SNumber)] + v `shouldMatchShapeExact` SObject [("foo", SNumber)] -- | Providing a non-string when 'SString' is expected causes a failure. testShapeWrongTypeString :: (HasCallStack) => App () testShapeWrongTypeString = do let v = Number 42 expectFailure (\_ -> pure ()) do - v `shouldMatchShape` SString + v `shouldMatchShapeExact` SString -- | An array element with the wrong type causes a failure, and the error -- message includes the element index. @@ -63,7 +69,7 @@ testShapeArrayElementMismatch = do -- First two elements are strings (match), third is a number (mismatch at [2]) let v = toJSON [toJSON ("a" :: String), toJSON ("b" :: String), toJSON (3 :: Int)] expectFailure (\e -> e.msg `shouldContainString` "[2]") do - v `shouldMatchShape` SArray SString + v `shouldMatchShapeExact` SArray SString -- | A nested mismatch deep in an object/array reports the full JSON path. testShapeNestedPathReported :: (HasCallStack) => App () @@ -80,7 +86,7 @@ testShapeNestedPathReported = do ] expectFailure (\e -> e.msg `shouldContainString` ".assets[0].key") do v - `shouldMatchShape` SObject + `shouldMatchShapeExact` SObject [ ( "assets", SArray ( SObject @@ -97,15 +103,15 @@ testShapeSAny :: (HasCallStack) => App () testShapeSAny = do let vals :: [Value] vals = [Null, Bool True, toJSON ("x" :: String), Number 1, toJSON ([] :: [Int]), object []] - mapM_ (`shouldMatchShape` SAny) vals + mapM_ (`shouldMatchShapeExact` SAny) vals -- | An empty array matches 'SArray' with any element shape. testShapeEmptyArray :: (HasCallStack) => App () testShapeEmptyArray = do let v = toJSON ([] :: [Int]) - v `shouldMatchShape` SArray SString - v `shouldMatchShape` SArray SNumber - v `shouldMatchShape` SArray (SObject []) + v `shouldMatchShapeExact` SArray SString + v `shouldMatchShapeExact` SArray SNumber + v `shouldMatchShapeExact` SArray (SObject []) -- | 'valueShape' computes the correct shape of a JSON value. testValueShape :: (HasCallStack) => App () @@ -120,4 +126,4 @@ testValueShape = do ] shape <- valueShape v -- The computed shape should itself pass the shape-match on v - v `shouldMatchShape` shape + v `shouldMatchShapeExact` shape diff --git a/integration/test/Test/Spar.hs b/integration/test/Test/Spar.hs index a39337c6d08..27e622fb8b3 100644 --- a/integration/test/Test/Spar.hs +++ b/integration/test/Test/Spar.hs @@ -25,7 +25,6 @@ import API.Common (defPassword, randomDomain, randomEmail, randomExternalId, ran import API.GalleyInternal (setTeamFeatureStatus) import API.Spar import API.SparInternal -import Control.Concurrent (threadDelay) import Control.Lens (to, (^.)) import qualified Data.Aeson as A import qualified Data.Aeson.KeyMap as KeyMap @@ -47,23 +46,55 @@ import Testlib.Prelude testSparUserCreationInvitationTimeout :: (HasCallStack) => App () testSparUserCreationInvitationTimeout = do - (owner, _tid, _) <- createTeam OwnDomain 1 + (owner, tid, _) <- createTeam OwnDomain 1 tok <- createScimTokenV6 owner def >>= \resp -> resp.json %. "token" >>= asString - scimUser <- randomScimUser - bindResponse (createScimUser OwnDomain tok scimUser) $ \res -> do - res.status `shouldMatchInt` 201 - -- Trying to create the same user again right away should fail - bindResponse (createScimUser OwnDomain tok scimUser) $ \res -> do - res.status `shouldMatchInt` 409 + email <- randomEmail + extId <- randomExternalId + scimUserToAcceptInvitation <- randomScimUserWithEmail extId email + scimUserToExpire <- randomScimUser + + uidToExpire <- bindResponse (createScimUser OwnDomain tok scimUserToExpire) $ \res -> do + res.status `shouldMatchInt` 201 + res.json %. "id" >>= asString - -- However, if we wait until the invitation timeout has passed - -- It's currently configured to 1s local/CI. - liftIO $ threadDelay (2_000_000) + dom <- asString OwnDomain + let quidToExpire = object ["id" .= uidToExpire, "domain" .= dom] - -- ...we should be able to create the user again - retryT $ bindResponse (createScimUser OwnDomain tok scimUser) $ \res -> do + uidToAcceptInvitation <- bindResponse (createScimUser OwnDomain tok scimUserToAcceptInvitation) $ \res -> do res.status `shouldMatchInt` 201 + res.json %. "id" >>= asString + + withWebSocket quidToExpire $ \expireWs -> do + -- Accept the invitation for one user + registerInvitedUser OwnDomain tid email + + -- Getting both users immediately succeeds + getScimUser owner tok uidToExpire >>= assertSuccess + getScimUser owner tok uidToAcceptInvitation >>= assertSuccess + + -- Trying to create the same user again right away should fail + bindResponse (createScimUser OwnDomain tok scimUserToExpire) $ \res -> do + res.status `shouldMatchInt` 409 + + bindResponse (createScimUser OwnDomain tok scimUserToAcceptInvitation) $ \res -> do + res.status `shouldMatchInt` 409 + + -- However, if we wait until the invitation timeout has passed. It's + -- currently configured to 1s local/CI, however we rely on the user actually + -- being deleted which happens in yet another background job, so we wait + -- until the user receives the delete event. + -- + -- Here we await any event because this user shouldn't receive any other + -- events. + void $ awaitAnyEvent 10 expireWs + + getScimUser owner tok uidToExpire >>= assertStatus 404 + getScimUser owner tok uidToAcceptInvitation >>= assertSuccess + + -- We should be able to create the user again + retryT $ bindResponse (createScimUser OwnDomain tok scimUserToExpire) $ \res -> do + res.status `shouldMatchInt` 201 testSparExternalIdDifferentFromEmailWithIdp :: (HasCallStack) => App () testSparExternalIdDifferentFromEmailWithIdp = do @@ -856,12 +887,12 @@ testSsoLoginAndEmailVerification = do user %. "email" `shouldMatch` email -- | This test may be covered by `testScimUpdateEmailAddress` and maybe can be removed. -testSsoLoginNoSamlEmailValidation :: (HasCallStack) => TaggedBool "validateSAMLEmails" -> App () -testSsoLoginNoSamlEmailValidation (TaggedBool validateSAMLEmails) = do +testSsoLoginNoSamlEmailValidation :: (HasCallStack) => TaggedBool "requireExternalEmailVerification" -> App () +testSsoLoginNoSamlEmailValidation (TaggedBool requireExternalEmailVerification) = do (owner, tid, _) <- createTeam OwnDomain 1 emailDomain <- randomDomain - let status = if validateSAMLEmails then "enabled" else "disabled" + let status = if requireExternalEmailVerification then "enabled" else "disabled" assertSuccess =<< setTeamFeatureStatus owner tid "validateSAMLemails" status void $ setTeamFeatureStatus owner tid "sso" "enabled" @@ -879,7 +910,7 @@ testSsoLoginNoSamlEmailValidation (TaggedBool validateSAMLEmails) = do eid = CI.original $ uref ^. SAML.uidSubject . to SAML.unsafeShowNameID eid `shouldMatch` email - when validateSAMLEmails $ do + when requireExternalEmailVerification $ do getUsersId OwnDomain [uid] `bindResponse` \res -> do res.status `shouldMatchInt` 200 user <- res.json & asList >>= assertOne @@ -905,11 +936,11 @@ testSsoLoginNoSamlEmailValidation (TaggedBool validateSAMLEmails) = do user %. "email" `shouldMatch` email -- | create user with non-email externalId. then use put to add an email address. -testScimUpdateEmailAddress :: (HasCallStack) => TaggedBool "extIdIsEmail" -> TaggedBool "validateSAMLEmails" -> App () -testScimUpdateEmailAddress (TaggedBool extIdIsEmail) (TaggedBool validateSAMLEmails) = do +testScimUpdateEmailAddress :: (HasCallStack) => TaggedBool "extIdIsEmail" -> TaggedBool "requireExternalEmailVerification" -> App () +testScimUpdateEmailAddress (TaggedBool extIdIsEmail) (TaggedBool requireExternalEmailVerification) = do (owner, tid, _) <- createTeam OwnDomain 1 - let status = if validateSAMLEmails then "enabled" else "disabled" + let status = if requireExternalEmailVerification then "enabled" else "disabled" assertSuccess =<< setTeamFeatureStatus owner tid "validateSAMLemails" status void $ setTeamFeatureStatus owner tid "sso" "enabled" @@ -960,7 +991,7 @@ testScimUpdateEmailAddress (TaggedBool extIdIsEmail) (TaggedBool validateSAMLEma res.status `shouldMatchInt` 200 res.json %. "emails" `shouldMatch` [object ["value" .= newEmail]] - when validateSAMLEmails $ do + when requireExternalEmailVerification $ do getUsersId OwnDomain [uid] `bindResponse` \res -> do res.status `shouldMatchInt` 200 user <- res.json & asList >>= assertOne @@ -1133,11 +1164,11 @@ testScimUpdateEmailAddressAndExternalId = do user %. "status" `shouldMatch` "active" user %. "email" `shouldMatch` newEmail1 -testScimLoginNoSamlEmailValidation :: (HasCallStack) => TaggedBool "validateSAMLEmails" -> App () -testScimLoginNoSamlEmailValidation (TaggedBool validateSAMLEmails) = do +testScimLoginNoSamlEmailValidation :: (HasCallStack) => TaggedBool "requireExternalEmailVerification" -> App () +testScimLoginNoSamlEmailValidation (TaggedBool requireExternalEmailVerification) = do (owner, tid, _) <- createTeam OwnDomain 1 - let status = if validateSAMLEmails then "enabled" else "disabled" + let status = if requireExternalEmailVerification then "enabled" else "disabled" assertSuccess =<< setTeamFeatureStatus owner tid "validateSAMLemails" status void $ setTeamFeatureStatus owner tid "sso" "enabled" @@ -1156,7 +1187,7 @@ testScimLoginNoSamlEmailValidation (TaggedBool validateSAMLEmails) = do res.status `shouldMatchInt` 200 res.json %. "id" `shouldMatch` uid - when validateSAMLEmails $ do + when requireExternalEmailVerification $ do getUsersId OwnDomain [uid] `bindResponse` \res -> do res.status `shouldMatchInt` 200 user <- res.json & asList >>= assertOne diff --git a/integration/test/Test/Spar/GetByEmail.hs b/integration/test/Test/Spar/GetByEmail.hs index 5aab06a905a..155bee47f48 100644 --- a/integration/test/Test/Spar/GetByEmail.hs +++ b/integration/test/Test/Spar/GetByEmail.hs @@ -28,10 +28,10 @@ import Testlib.Prelude -- | Test the /sso/get-by-email endpoint with multi-ingress setup testGetSsoCodeByEmailWithMultiIngress :: (HasCallStack) => - TaggedBool "validateSAMLemails" -> + TaggedBool "requireExternalEmailVerification" -> TaggedBool "idpScimToken" -> App () -testGetSsoCodeByEmailWithMultiIngress (TaggedBool validateSAMLemails) (TaggedBool isIdPScimToken) = do +testGetSsoCodeByEmailWithMultiIngress (TaggedBool requireExternalEmailVerification) (TaggedBool isIdPScimToken) = do let ernieZHost = "nginz-https.ernie.example.com" bertZHost = "nginz-https.bert.example.com" @@ -65,13 +65,13 @@ testGetSsoCodeByEmailWithMultiIngress (TaggedBool validateSAMLemails) (TaggedBoo assertSuccess =<< setTeamFeatureStatus domain tid "sso" "enabled" -- The test should work for both: SCIM user with and without email confirmation - let status = if validateSAMLemails then "enabled" else "disabled" + let status = if requireExternalEmailVerification then "enabled" else "disabled" assertSuccess =<< setTeamFeatureStatus owner tid "validateSAMLemails" status -- Create IdP for ernie domain SAML.SampleIdP idpmetaErnie _ _ _ <- SAML.makeSampleIdPMetadata idpIdErnie <- - createIdpWithZHost owner (Just ernieZHost) idpmetaErnie `bindResponse` \resp -> do + createIdpWithZHostV2 owner (Just ernieZHost) idpmetaErnie `bindResponse` \resp -> do resp.status `shouldMatchInt` 201 resp.json %. "extraInfo.domain" `shouldMatch` ernieZHost resp.json %. "id" >>= asString @@ -79,7 +79,7 @@ testGetSsoCodeByEmailWithMultiIngress (TaggedBool validateSAMLemails) (TaggedBoo -- Create IdP for bert domain SAML.SampleIdP idpmetaBert _ _ _ <- SAML.makeSampleIdPMetadata idpIdBert <- - createIdpWithZHost owner (Just bertZHost) idpmetaBert `bindResponse` \resp -> do + createIdpWithZHostV2 owner (Just bertZHost) idpmetaBert `bindResponse` \resp -> do resp.status `shouldMatchInt` 201 resp.json %. "extraInfo.domain" `shouldMatch` bertZHost resp.json %. "id" >>= asString @@ -98,7 +98,7 @@ testGetSsoCodeByEmailWithMultiIngress (TaggedBool validateSAMLemails) (TaggedBoo createScimUser domain scimToken scimUser >>= assertSuccess if isIdPScimToken - then when validateSAMLemails $ do + then when requireExternalEmailVerification $ do -- Activate the email so the user can be found by email activateEmail domain userEmail else @@ -124,15 +124,15 @@ testGetSsoCodeByEmailWithMultiIngress (TaggedBool validateSAMLemails) (TaggedBoo ssoCodeStr `shouldMatch` idpIdBert -- | Test the /sso/get-by-email endpoint with regular (non-multi-ingress) setup -testGetSsoCodeByEmailRegular :: (HasCallStack) => (TaggedBool "validateSAMLemails") -> (TaggedBool "idpScimToken") -> App () -testGetSsoCodeByEmailRegular (TaggedBool validateSAMLemails) (TaggedBool isIdPScimToken) = +testGetSsoCodeByEmailRegular :: (HasCallStack) => (TaggedBool "requireExternalEmailVerification") -> (TaggedBool "idpScimToken") -> App () +testGetSsoCodeByEmailRegular (TaggedBool requireExternalEmailVerification) (TaggedBool isIdPScimToken) = withModifiedBackend def {sparCfg = setField "enableIdPByEmailDiscovery" True} $ \domain -> do (owner, tid, _) <- createTeam domain 1 void $ setTeamFeatureStatus owner tid "sso" "enabled" -- The test should work for both: SCIM user with and without email confirmation - let status = if validateSAMLemails then "enabled" else "disabled" + let status = if requireExternalEmailVerification then "enabled" else "disabled" assertSuccess =<< setTeamFeatureStatus owner tid "validateSAMLemails" status -- Create IdP without domain binding @@ -156,7 +156,7 @@ testGetSsoCodeByEmailRegular (TaggedBool validateSAMLemails) (TaggedBool isIdPSc createScimUser domain scimToken scimUser >>= assertSuccess if isIdPScimToken - then when validateSAMLemails $ do + then when requireExternalEmailVerification $ do -- Activate the email so the user can be found by email activateEmail domain userEmail else @@ -267,7 +267,7 @@ testGetSsoCodeByEmailDisabledMultiIngress = do -- Create IdP for ernie domain SAML.SampleIdP idpmetaErnie _ _ _ <- SAML.makeSampleIdPMetadata idpIdErnie <- - createIdpWithZHost owner (Just ernieZHost) idpmetaErnie `bindResponse` \resp -> do + createIdpWithZHostV2 owner (Just ernieZHost) idpmetaErnie `bindResponse` \resp -> do resp.status `shouldMatchInt` 201 resp.json %. "extraInfo.domain" `shouldMatch` ernieZHost resp.json %. "id" >>= asString diff --git a/integration/test/Test/Spar/MultiIngressIdp.hs b/integration/test/Test/Spar/MultiIngressIdp.hs index 61bc4967177..58960b5573a 100644 --- a/integration/test/Test/Spar/MultiIngressIdp.hs +++ b/integration/test/Test/Spar/MultiIngressIdp.hs @@ -38,7 +38,8 @@ testMultiIngressIdpSimpleCase = do "saml.spDomainConfigs" ( object [ ernieZHost .= makeSpDomainConfig ernieZHost, - bertZHost .= makeSpDomainConfig bertZHost + bertZHost .= makeSpDomainConfig bertZHost, + kermitZHost .= makeSpDomainConfig kermitZHost ] ) } @@ -49,7 +50,7 @@ testMultiIngressIdpSimpleCase = do -- Create IdP for one domain SAML.SampleIdP idpmeta _ _ _ <- SAML.makeSampleIdPMetadata idpId <- - createIdpWithZHost owner (Just ernieZHost) idpmeta `bindResponse` \resp -> do + createIdpWithZHostV2 owner (Just ernieZHost) idpmeta `bindResponse` \resp -> do resp.status `shouldMatchInt` 201 resp.json %. "extraInfo.domain" `shouldMatch` ernieZHost resp.json %. "id" >>= asString @@ -91,7 +92,7 @@ testUnconfiguredDomain = forM_ [Nothing, Just kermitZHost] $ \unconfiguredZHost SAML.SampleIdP idpmeta1 _ _ _ <- SAML.makeSampleIdPMetadata idpId1 <- - createIdpWithZHost owner (Just ernieZHost) idpmeta1 `bindResponse` \resp -> do + createIdpWithZHostV2 owner (Just ernieZHost) idpmeta1 `bindResponse` \resp -> do resp.status `shouldMatchInt` 201 resp.json %. "extraInfo.domain" `shouldMatch` ernieZHost resp.json %. "id" >>= asString @@ -117,7 +118,7 @@ testUnconfiguredDomain = forM_ [Nothing, Just kermitZHost] $ \unconfiguredZHost -- Create unconfigured -> no multi-ingress domain SAML.SampleIdP idpmeta2 _ _ _ <- SAML.makeSampleIdPMetadata idpId2 <- - createIdpWithZHost owner (unconfiguredZHost) idpmeta2 `bindResponse` \resp -> do + createIdpWithZHostV2 owner (unconfiguredZHost) idpmeta2 `bindResponse` \resp -> do resp.status `shouldMatchInt` 201 resp.json %. "extraInfo.domain" `shouldMatch` Null resp.json %. "id" >>= asString @@ -129,7 +130,7 @@ testUnconfiguredDomain = forM_ [Nothing, Just kermitZHost] $ \unconfiguredZHost -- Create a second unconfigured -> no multi-ingress domain SAML.SampleIdP idpmeta3 _ _ _ <- SAML.makeSampleIdPMetadata idpId3 <- - createIdpWithZHost owner (unconfiguredZHost) idpmeta3 `bindResponse` \resp -> do + createIdpWithZHostV2 owner (unconfiguredZHost) idpmeta3 `bindResponse` \resp -> do resp.status `shouldMatchInt` 201 resp.json %. "extraInfo.domain" `shouldMatch` Null resp.json %. "id" >>= asString @@ -150,7 +151,8 @@ testMultiIngressAtMostOneIdPPerDomain = do "saml.spDomainConfigs" ( object [ ernieZHost .= makeSpDomainConfig ernieZHost, - bertZHost .= makeSpDomainConfig bertZHost + bertZHost .= makeSpDomainConfig bertZHost, + kermitZHost .= makeSpDomainConfig kermitZHost ] ) } @@ -160,21 +162,21 @@ testMultiIngressAtMostOneIdPPerDomain = do SAML.SampleIdP idpmeta1 _ _ _ <- SAML.makeSampleIdPMetadata idpId1 <- - createIdpWithZHost owner (Just ernieZHost) idpmeta1 `bindResponse` \resp -> do + createIdpWithZHostV2 owner (Just ernieZHost) idpmeta1 `bindResponse` \resp -> do resp.status `shouldMatchInt` 201 resp.json %. "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 + createIdpWithZHostV2 owner (Just ernieZHost) idpmeta2 `bindResponse` \resp -> do resp.status `shouldMatchInt` 409 resp.json %. "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 + createIdpWithZHostV2 owner (Just bertZHost) idpmeta2 `bindResponse` \resp -> do resp.status `shouldMatchInt` 201 resp.json %. "id" >>= asString @@ -186,7 +188,7 @@ testMultiIngressAtMostOneIdPPerDomain = do -- 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 + createIdpWithZHostV2 owner Nothing idpmeta4 `bindResponse` \resp -> do resp.status `shouldMatchInt` 201 resp.json %. "id" >>= asString @@ -213,14 +215,14 @@ testMultiIngressAtMostOneIdPPerDomain = do SAML.SampleIdP idpmeta5 _ _ _ <- SAML.makeSampleIdPMetadata idpId5 <- - createIdpWithZHost owner (Just ernieZHost) idpmeta5 `bindResponse` \resp -> do + createIdpWithZHostV2 owner (Just ernieZHost) idpmeta5 `bindResponse` \resp -> do resp.status `shouldMatchInt` 201 resp.json %. "extraInfo.domain" `shouldMatch` ernieZHost resp.json %. "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 + createIdpWithZHostV2 owner (Just bertZHost) idpmeta6 `bindResponse` \resp -> do resp.status `shouldMatchInt` 409 resp.json %. "label" `shouldMatch` "idp-duplicate-domain-for-team" @@ -228,7 +230,7 @@ testMultiIngressAtMostOneIdPPerDomain = do resp.status `shouldMatchInt` 204 idpId6 <- - createIdpWithZHost owner (Just bertZHost) idpmeta6 `bindResponse` \resp -> do + createIdpWithZHostV2 owner (Just bertZHost) idpmeta6 `bindResponse` \resp -> do resp.status `shouldMatchInt` 201 resp.json %. "extraInfo.domain" `shouldMatch` bertZHost resp.json %. "id" >>= asString @@ -254,14 +256,14 @@ testNonMultiIngressSetupsCanHaveMoreIdPsPerDomain = do -- With Z-Host header SAML.SampleIdP idpmeta1 _ _ _ <- SAML.makeSampleIdPMetadata idpId1 <- - createIdpWithZHost owner (Just ernieZHost) idpmeta1 `bindResponse` \resp -> do + createIdpWithZHostV2 owner (Just ernieZHost) idpmeta1 `bindResponse` \resp -> do resp.status `shouldMatchInt` 201 resp.json %. "extraInfo.domain" `shouldMatch` Null resp.json %. "id" >>= asString SAML.SampleIdP idpmeta2 _ _ _ <- SAML.makeSampleIdPMetadata idpId2 <- - createIdpWithZHost owner (Just ernieZHost) idpmeta2 `bindResponse` \resp -> do + createIdpWithZHostV2 owner (Just ernieZHost) idpmeta2 `bindResponse` \resp -> do resp.status `shouldMatchInt` 201 resp.json %. "extraInfo.domain" `shouldMatch` Null resp.json %. "id" >>= asString @@ -279,14 +281,14 @@ testNonMultiIngressSetupsCanHaveMoreIdPsPerDomain = do -- Without Z-Host header SAML.SampleIdP idpmeta5 _ _ _ <- SAML.makeSampleIdPMetadata idpId5 <- - createIdpWithZHost owner Nothing idpmeta5 `bindResponse` \resp -> do + createIdpWithZHostV2 owner Nothing idpmeta5 `bindResponse` \resp -> do resp.status `shouldMatchInt` 201 resp.json %. "extraInfo.domain" `shouldMatch` Null resp.json %. "id" >>= asString SAML.SampleIdP idpmeta6 _ _ _ <- SAML.makeSampleIdPMetadata idpId6 <- - createIdpWithZHost owner Nothing idpmeta6 `bindResponse` \resp -> do + createIdpWithZHostV2 owner Nothing idpmeta6 `bindResponse` \resp -> do resp.status `shouldMatchInt` 201 resp.json %. "extraInfo.domain" `shouldMatch` Null resp.json %. "id" >>= asString @@ -300,3 +302,117 @@ testNonMultiIngressSetupsCanHaveMoreIdPsPerDomain = do updateIdpWithZHost owner Nothing idpId6 idpmeta8 `bindResponse` \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "extraInfo.domain" `shouldMatch` Null + +-- | The `validateNewIdP` rules for IdP creation apply to multi-ingress setups as +-- well. Depending on the IdP API version, IdP issuers have to be either unique +-- per backend (V1) or per team (V2). +-- +-- Note: In multi-ingress setups, one might wonder why the same IdP metadata / +-- issuer cannot be used for the same team across multiple domains. Supporting +-- this would require redesigning spar's database schema (e.g., there would be +-- a race condition on the `spar.issuer_idp_v2` table). Furthermore, IdP +-- configs are strongly URL-related on IdP-side: issuers correspond to e.g. +-- Keycloak realms, which have a specific return-URL. Given the limited +-- practical benefit, this complexity is not justified for now. +testMultiIngressIdPIssuerDifferentDomains :: (HasCallStack) => App () +testMultiIngressIdPIssuerDifferentDomains = do + withModifiedBackend + def + { sparCfg = + removeField "saml.spSsoUri" + >=> removeField "saml.spAppUri" + >=> removeField "saml.contacts" + >=> setField + "saml.spDomainConfigs" + ( object + [ ernieZHost .= makeSpDomainConfig ernieZHost, + bertZHost .= makeSpDomainConfig bertZHost, + kermitZHost .= makeSpDomainConfig kermitZHost + ] + ) + } + $ \domain -> do + -- V1 API: Issuers must be unique per backend (across all teams) + (owner1, tid1, _) <- createTeam domain 1 + void $ setTeamFeatureStatus owner1 tid1 "sso" "enabled" + + -- Create first IdP metadata for V1 + SAML.SampleIdP idpmetaV1 _ _ _ <- SAML.makeSampleIdPMetadata + _idpId1 <- + createIdpWithZHostV1 owner1 (Just ernieZHost) idpmetaV1 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.json %. "extraInfo.domain" `shouldMatch` ernieZHost + resp.json %. "id" >>= asString + + -- Try to create V1 IdP on a different team with different metadata but same issuer -> failure + -- Test with different domains to show constraint is domain-independent + (owner2, tid2, _) <- createTeam domain 1 + void $ setTeamFeatureStatus owner2 tid2 "sso" "enabled" + + -- Try with same domain as original -> should fail (V1 global uniqueness) + SAML.SampleIdP idpmetaV1_alt _ _ _ <- SAML.makeSampleIdPMetadata + let idpmetaV1_alt_sameIssuer = idpmetaV1_alt & SAML.edIssuer .~ (idpmetaV1 ^. SAML.edIssuer) + + createIdpWithZHostV1 owner2 (Just ernieZHost) idpmetaV1_alt_sameIssuer `bindResponse` \resp -> do + resp.status `shouldMatchInt` 400 + resp.json %. "label" `shouldMatch` "idp-already-in-use" + + -- Try with different domain -> should also fail (V1 global uniqueness) + createIdpWithZHostV1 owner2 (Just bertZHost) idpmetaV1_alt_sameIssuer `bindResponse` \resp -> do + resp.status `shouldMatchInt` 400 + resp.json %. "label" `shouldMatch` "idp-already-in-use" + + -- Try with no domain -> should also fail (V1 global uniqueness) + createIdpWithZHostV1 owner2 Nothing idpmetaV1_alt_sameIssuer `bindResponse` \resp -> do + resp.status `shouldMatchInt` 400 + resp.json %. "label" `shouldMatch` "idp-already-in-use" + + -- Counter-example: V1 IdP with different issuer -> success + SAML.SampleIdP idpmetaV1_differentIssuer _ _ _ <- SAML.makeSampleIdPMetadata + void + $ createIdpWithZHostV1 owner2 (Just ernieZHost) idpmetaV1_differentIssuer + `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + + -- V2 API: Issuers must be unique per team (but can be reused across teams) + -- Use a different issuer than V1 to avoid API version mixing errors + (owner3, tid3, _) <- createTeam domain 1 + void $ setTeamFeatureStatus owner3 tid3 "sso" "enabled" + + -- Create V2 IdP on team 3 with new issuer + SAML.SampleIdP idpmetaV2 _ _ _ <- SAML.makeSampleIdPMetadata + + _idpId3 <- + createIdpWithZHostV2 owner3 (Just ernieZHost) idpmetaV2 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.json %. "extraInfo.domain" `shouldMatch` ernieZHost + resp.json %. "id" >>= asString + + -- Try to create another V2 IdP on same team with different metadata but same issuer -> failure + -- First, try with the same domain -> hits domain constraint (409) + SAML.SampleIdP idpmetaV2_alt _ _ _ <- SAML.makeSampleIdPMetadata + let idpmetaV2_alt_sameIssuer = idpmetaV2_alt & SAML.edIssuer .~ (idpmetaV2 ^. SAML.edIssuer) + + createIdpWithZHostV2 owner3 (Just ernieZHost) idpmetaV2_alt_sameIssuer `bindResponse` \resp -> do + resp.status `shouldMatchInt` 409 + resp.json %. "label" `shouldMatch` "idp-duplicate-domain-for-team" + + -- Try with a different domain -> hits issuer constraint (400) + createIdpWithZHostV2 owner3 (Just bertZHost) idpmetaV2_alt_sameIssuer `bindResponse` \resp -> do + resp.status `shouldMatchInt` 400 + resp.json %. "label" `shouldMatch` "idp-already-in-use" + + -- Try with no domain -> hits issuer constraint (400) + createIdpWithZHostV2 owner3 Nothing idpmetaV2_alt_sameIssuer `bindResponse` \resp -> do + resp.status `shouldMatchInt` 400 + resp.json %. "label" `shouldMatch` "idp-already-in-use" + + -- Counter-example: V2 IdP with same issuer on different team -> success (different team) + (owner4, tid4, _) <- createTeam domain 1 + void $ setTeamFeatureStatus owner4 tid4 "sso" "enabled" + + void + $ createIdpWithZHostV2 owner4 (Just ernieZHost) idpmetaV2_alt_sameIssuer + `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.json %. "extraInfo.domain" `shouldMatch` ernieZHost diff --git a/integration/test/Test/Spar/STM.hs b/integration/test/Test/Spar/STM.hs index cbf8fb0c3a0..5ad0a151eff 100644 --- a/integration/test/Test/Spar/STM.hs +++ b/integration/test/Test/Spar/STM.hs @@ -112,7 +112,7 @@ runSteps :: (HasCallStack) => [Step] -> App () runSteps steps = do (owner, tid, []) <- createTeam OwnDomain 1 void $ setTeamFeatureStatus owner tid "sso" "enabled" - void $ setTeamFeatureStatus owner tid "validateSAMLEmails" "enabled" + void $ setTeamFeatureStatus owner tid "validateSAMLemails" "enabled" go owner tid emptyState steps where go :: Value -> String -> State -> [Step] -> App () diff --git a/integration/test/Test/Swagger.hs b/integration/test/Test/Swagger.hs index f11d8c4d125..82ba66b8ddb 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, 15] +existingVersions = Set.fromList [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] internalApis :: Set String internalApis = Set.fromList ["brig", "cannon", "cargohold", "cannon", "spar"] diff --git a/integration/test/Test/User.hs b/integration/test/Test/User.hs index 0c8ff60eba9..4124cd129fb 100644 --- a/integration/test/Test/User.hs +++ b/integration/test/Test/User.hs @@ -418,3 +418,12 @@ testEphemeralUserCreation (TaggedBool enabled) = do where registerEphemeralUser domain = addUser domain def registerUserWithEmail domain = addUser domain def {email = Just ("user@" <> domain)} + +testSuspendNonExistingUser :: (HasCallStack) => App () +testSuspendNonExistingUser = do + existingUser <- randomUser OwnDomain def + uid <- randomId + dom <- asString OwnDomain + let quid = object ["domain" .= dom, "id" .= uid] + I.setAccountStatus quid "suspended" >>= assertStatus 404 + getUser existingUser quid >>= assertStatus 404 diff --git a/integration/test/Testlib/Assertions.hs b/integration/test/Testlib/Assertions.hs index 8f13eb96c75..1603d7cf57f 100644 --- a/integration/test/Testlib/Assertions.hs +++ b/integration/test/Testlib/Assertions.hs @@ -40,6 +40,7 @@ import qualified Data.ByteString.Lazy as BS import Data.Char import Data.Foldable import Data.Hex +import Data.IORef import Data.List import qualified Data.Map as Map import Data.Maybe @@ -50,6 +51,7 @@ import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Encoding as TL import GHC.Stack as Stack import qualified Network.HTTP.Client as HTTP +import System.Environment import System.FilePath import Testlib.JSON import Testlib.Printing @@ -269,6 +271,19 @@ shouldMatchOneOf a b = do pb <- prettyJSON b assertFailure $ "Expected:\n" <> pa <> "\n to match at least one of:\n" <> pb +shouldNotMatchOneOf :: + (MakesValue a, MakesValue b, HasCallStack) => + a -> + b -> + App () +shouldNotMatchOneOf a b = do + lb <- asList b + xa <- make a + when (xa `elem` lb) $ do + pa <- prettyJSON a + pb <- prettyJSON b + assertFailure $ "Expected:\n" <> pa <> "\n to not match any of:\n" <> pb + ---------------------------------------------------------------------- -- Shape DSL @@ -293,38 +308,64 @@ data Shape -- | Assert that @actual@ conforms to @shape@. Provides a JSON-path-like -- location in the failure message (e.g. @.assets[0].key@). -shouldMatchShape :: +shouldMatchShapeExact :: + (MakesValue a, HasCallStack) => + -- | The actual value + a -> + -- | The expected shape + Shape -> + App () +shouldMatchShapeExact = shouldMatchShapeImpl Reject + +-- | Assert that @actual@ conforms to @shape@. Provides a JSON-path-like +-- location in the failure message (e.g. @.assets[0].key@). +shouldMatchShapeLenient :: (MakesValue a, HasCallStack) => -- | The actual value a -> -- | The expected shape Shape -> App () -shouldMatchShape a shape = do +shouldMatchShapeLenient = shouldMatchShapeImpl Allow + +-- | Assert that @actual@ conforms to @shape@. Provides a JSON-path-like +-- location in the failure message (e.g. @.assets[0].key@). +shouldMatchShapeImpl :: + (MakesValue a, HasCallStack) => + UnknownAttributes -> + -- | The actual value + a -> + -- | The expected shape + Shape -> + App () +shouldMatchShapeImpl unknownAttributes a shape = do val <- make a - case matchShape "" val shape of + case matchShape unknownAttributes "" val shape of Nothing -> pure () Just err -> assertFailure $ "Shape mismatch" <> err +data UnknownAttributes = Allow | Reject + deriving (Eq, Show) + -- | Internal recursive shape-matcher. Returns 'Nothing' on success and -- @'Just' errorMessage@ on failure. The @path@ argument accumulates the -- JSON-path-like location prefix. -matchShape :: String -> Value -> Shape -> Maybe String -matchShape _ _ SAny = Nothing -matchShape _ Aeson.Null SNull = Nothing -matchShape path _ SNull = Just $ " at " <> matchShapeLoc path <> ": expected null" -matchShape _ (Aeson.Bool _) SBool = Nothing -matchShape path _ SBool = Just $ " at " <> matchShapeLoc path <> ": expected bool" -matchShape _ (Aeson.String _) SString = Nothing -matchShape path _ SString = Just $ " at " <> matchShapeLoc path <> ": expected string" -matchShape _ (Aeson.Number _) SNumber = Nothing -matchShape path _ SNumber = Just $ " at " <> matchShapeLoc path <> ": expected number" -matchShape path (Aeson.Array arr) (SArray elemShape) = +matchShape :: UnknownAttributes -> String -> Value -> Shape -> Maybe String +matchShape _ _ _ SAny = Nothing +matchShape _ _ Aeson.Null SNull = Nothing +matchShape _ path _ SNull = Just $ " at " <> matchShapeLoc path <> ": expected null" +matchShape _ _ (Aeson.Bool _) SBool = Nothing +matchShape _ path _ SBool = Just $ " at " <> matchShapeLoc path <> ": expected bool" +matchShape _ _ (Aeson.String _) SString = Nothing +matchShape _ path _ SString = Just $ " at " <> matchShapeLoc path <> ": expected string" +matchShape _ _ (Aeson.Number _) SNumber = Nothing +matchShape _ path _ SNumber = Just $ " at " <> matchShapeLoc path <> ": expected number" +matchShape unknownAttributes path (Aeson.Array arr) (SArray elemShape) = listToMaybe - . mapMaybe (\(i, v) -> matchShape (path <> "[" <> show (i :: Int) <> "]") v elemShape) + . mapMaybe (\(i, v) -> matchShape unknownAttributes (path <> "[" <> show (i :: Int) <> "]") v elemShape) $ zip [0 ..] (toList arr) -matchShape path _ (SArray _) = Just $ " at " <> matchShapeLoc path <> ": expected array" -matchShape path (Aeson.Object obj) (SObject fields) = +matchShape _ path _ (SArray _) = Just $ " at " <> matchShapeLoc path <> ": expected array" +matchShape unknownAttributes path (Aeson.Object obj) (SObject fields) = let objPairs = [(Key.toString k, v) | (k, v) <- Aeson.toList obj] actualKeys = map fst objPairs expectedKeys = map fst fields @@ -332,15 +373,15 @@ matchShape path (Aeson.Object obj) (SObject fields) = missingKeys = expectedKeys \\ actualKeys go (k, s) = case lookup k objPairs of Nothing -> Nothing -- already checked above - Just v -> matchShape (path <> "." <> k) v s - in case (unexpectedKeys, missingKeys) of - (k : _, _) -> - Just $ " at " <> matchShapeLoc path <> ": unexpected key \"" <> k <> "\"" - (_, k : _) -> - Just $ " at " <> matchShapeLoc path <> ": missing key \"" <> k <> "\"" + Just v -> matchShape unknownAttributes (path <> "." <> k) v s + in case (unknownAttributes, unexpectedKeys, missingKeys) of + (Reject, ks@(_ : _), _) -> + Just $ " at " <> matchShapeLoc path <> ": unexpected keys \"" <> show ks <> "\"" + (_, _, ks@(_ : _)) -> + Just $ " at " <> matchShapeLoc path <> ": missing keys \"" <> show ks <> "\"" _ -> listToMaybe . mapMaybe go $ fields -matchShape path _ (SObject _) = Just $ " at " <> matchShapeLoc path <> ": expected object" +matchShape _ path _ (SObject _) = Just $ " at " <> matchShapeLoc path <> ": expected object" -- | Format a path for use in error messages, using the document root (@$@) -- when the path is empty. @@ -400,24 +441,37 @@ super `shouldNotContain` sub = do when (sub `isInfixOf` super) $ do assertFailure $ "String or List:\n" <> show super <> "\nDoes contain:\n" <> show sub -printFailureDetails :: AssertionFailure -> IO String -printFailureDetails (AssertionFailure stack mbResponse ctx msg) = do +printFailureDetails :: Env -> AssertionFailure -> IO String +printFailureDetails env (AssertionFailure stack mbResponse ctx msg) = do s <- prettierCallStack stack + ct <- renderCurlTrace env.curlTrace pure . unlines $ colored yellow "assertion failure:" : colored red msg : "\n" <> s : toList (fmap prettyResponse mbResponse) <> toList (fmap prettyContext ctx) + <> ct -printAppFailureDetails :: AppFailure -> IO String -printAppFailureDetails (AppFailure msg stack) = do +printAppFailureDetails :: Env -> AppFailure -> IO String +printAppFailureDetails env (AppFailure msg stack) = do s <- prettierCallStack stack + ct <- renderCurlTrace env.curlTrace pure . unlines $ colored yellow "app failure:" : colored red msg : "\n" : [s] + <> ct + +renderCurlTrace :: IORef [String] -> IO [String] +renderCurlTrace trace = do + isTestVerbose >>= \case + True -> ("HTTP trace in curl pseudo-syntax:" :) <$> readIORef trace + False -> pure ["Set WIRE_INTEGRATION_TEST_VERBOSITY=1 if you want to see complete trace of the HTTP traffic in curl pseudo-syntax."] + +isTestVerbose :: (MonadIO m) => m Bool +isTestVerbose = liftIO $ (Just "1" ==) <$> lookupEnv "WIRE_INTEGRATION_TEST_VERBOSITY" prettyContext :: String -> String prettyContext ctx = do @@ -426,12 +480,14 @@ prettyContext ctx = do colored blue ctx ] -printExceptionDetails :: SomeException -> IO String -printExceptionDetails e = do +printExceptionDetails :: Env -> SomeException -> IO String +printExceptionDetails env e = do + ct <- renderCurlTrace env.curlTrace pure . unlines $ [ colored yellow "exception:", colored red (displayException e) ] + <> ct prettierCallStack :: CallStack -> IO String prettierCallStack cstack = do diff --git a/integration/test/Testlib/Env.hs b/integration/test/Testlib/Env.hs index 1a65d7f5ea1..7d7ad40703e 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 = 15, + gDefaultAPIVersion = 16, gManager = manager, gServicesCwdBase = devEnvProjectRoot <&> ( "services"), gBackendResourcePool = resourcePool, @@ -169,6 +169,7 @@ mkEnv currentTestName ge = do liftIO $ do pks <- newIORef (zip [1 ..] somePrekeys) lpks <- newIORef someLastPrekeys + curlTrace <- newIORef [] pure Env { serviceMap = gServiceMap ge, @@ -201,7 +202,8 @@ mkEnv currentTestName ge = do dnsMockServerConfig = ge.gDNSMockServerConfig, cellsEventQueue = ge.gCellsEventQueue, cellsEventWatchersLock = ge.gCellsEventWatchersLock, - cellsEventWatchers = ge.gCellsEventWatchers + cellsEventWatchers = ge.gCellsEventWatchers, + curlTrace } allCiphersuites :: [Ciphersuite] diff --git a/integration/test/Testlib/HTTP.hs b/integration/test/Testlib/HTTP.hs index 9ce4f542ada..643ff624f12 100644 --- a/integration/test/Testlib/HTTP.hs +++ b/integration/test/Testlib/HTTP.hs @@ -18,6 +18,7 @@ module Testlib.HTTP where import qualified Control.Exception as E +import Control.Monad.Extra (whenM) import Control.Monad.Reader import qualified Data.Aeson as Aeson import qualified Data.Aeson.Types as Aeson @@ -27,6 +28,7 @@ import qualified Data.ByteString.Char8 as C8 import qualified Data.ByteString.Lazy as L import qualified Data.CaseInsensitive as CI import Data.Function +import Data.IORef import Data.List import Data.List.Split (splitOn) import qualified Data.Map as Map @@ -231,19 +233,17 @@ zHost = addHeader "Z-Host" submit :: String -> HTTP.Request -> App Response submit method req0 = do - let req = req0 {HTTP.method = T.encodeUtf8 (T.pack method)} - -- uncomment this for more debugging noise: - -- liftIO $ putStrLn $ requestToCurl req + let request = req0 {HTTP.method = T.encodeUtf8 (T.pack method)} manager <- asks (.manager) - res <- liftIO $ HTTP.httpLbs req manager - pure $ - Response - { json = Aeson.decode (HTTP.responseBody res), - body = L.toStrict (HTTP.responseBody res), - status = HTTP.statusCode (HTTP.responseStatus res), - headers = HTTP.responseHeaders res, - request = req - } + response <- liftIO $ HTTP.httpLbs request manager + let json = Aeson.decode (HTTP.responseBody response) + body = L.toStrict (HTTP.responseBody response) + status = HTTP.statusCode (HTTP.responseStatus response) + headers = HTTP.responseHeaders response + whenM isTestVerbose do + curl <- asks (.curlTrace) + liftIO $ modifyIORef' curl (<> [requestToCurl request, "# ==> " <> show (status, body, headers), ""]) + pure Response {..} locationHeaderHost :: Response -> String locationHeaderHost resp = diff --git a/integration/test/Testlib/Run.hs b/integration/test/Testlib/Run.hs index 0566cc00f22..b32b9a78eb6 100644 --- a/integration/test/Testlib/Run.hs +++ b/integration/test/Testlib/Run.hs @@ -68,11 +68,11 @@ runTest testName ge action = lowerCodensity $ do -- This ensures things like UserInterrupt are properly handled. E.throw e, E.Handler -- AssertionFailure - (fmap Left . printFailureDetails), + (fmap Left . printFailureDetails env), E.Handler -- AppFailure - (fmap Left . printAppFailureDetails), + (fmap Left . printAppFailureDetails env), E.Handler - (fmap Left . printExceptionDetails) + (fmap Left . printExceptionDetails env) ] pluralise :: Int -> String -> String diff --git a/integration/test/Testlib/Types.hs b/integration/test/Testlib/Types.hs index 74ac08535c3..9f1d10c13bd 100644 --- a/integration/test/Testlib/Types.hs +++ b/integration/test/Testlib/Types.hs @@ -36,6 +36,7 @@ import Data.Aeson import qualified Data.Aeson as Aeson import Data.ByteString (ByteString) import qualified Data.ByteString as BS +import qualified Data.ByteString.Base64 as Base64 import qualified Data.ByteString.Char8 as C8 import qualified Data.ByteString.Lazy as L import qualified Data.CaseInsensitive as CI @@ -49,6 +50,7 @@ import qualified Data.Map as Map import Data.Set (Set) import qualified Data.Set as Set import Data.String +import Data.String.Conversions (cs) import qualified Data.Text as T import qualified Data.Text.Encoding as T import Data.Time @@ -271,7 +273,8 @@ data Env = Env dnsMockServerConfig :: DNSMockServerConfig, cellsEventQueue :: String, cellsEventWatchersLock :: MVar (), - cellsEventWatchers :: IORef (Map String QueueWatcher) + cellsEventWatchers :: IORef (Map String QueueWatcher), + curlTrace :: IORef [String] } data Response = Response @@ -374,7 +377,7 @@ data MLSConv = MLSConv requestToCurl :: HTTP.Request -> String requestToCurl req = - unwords $ -- FUTUREWORK: amke this multi-line, but so thhhaaaatttt iiiitttt ddddoooesn't go wrong. + unwords $ -- FUTUREWORK: make this multi-line, but so thhhaaaatttt iiiitttt ddddoooesn't go wrong. Prelude.filter (not . Prelude.null) [ "curl", @@ -401,11 +404,29 @@ requestToCurl req = defaultPort = if HTTP.secure req then 443 else 80 body' = case HTTP.requestBody req of - HTTP.RequestBodyLBS lbs -> if lbs == mempty then "" else "--data-binary " ++ shellEscape (C8.unpack $ L.toStrict lbs) - HTTP.RequestBodyBS bs -> if bs == mempty then "" else "--data-binary " ++ shellEscape (C8.unpack bs) - HTTP.RequestBodyBuilder _ _ -> "--data-binary ''" - _ -> "" - + HTTP.RequestBodyLBS lbs -> dataBinary (C8.unpack $ L.toStrict lbs) + HTTP.RequestBodyBS bs -> dataBinary (C8.unpack bs) + _ -> + -- this won't work + "--data-binary ''" + + dataBinary :: String -> String + dataBinary "" = "" + dataBinary raw = + case Aeson.decode @Aeson.Value (cs raw) of + -- For JSON bodies, pass the payload directly, properly shell-escaped. + Just _val -> + "--data-binary " <> shellEscape raw + -- For non-JSON (potentially binary) bodies, use a base64 literal + -- and decode it at runtime via a valid command substitution. + Nothing -> + let b64 :: String + b64 = cs (Base64.encode (cs raw)) + in "--data-binary \"$(printf %s " <> shellEscape b64 <> " | base64 -d)\"" + + -- this is probably used wrong, and there are still some escape + -- issues to be solved. but it should be safe as long as we're + -- only using it in our own integration tests, right? shellEscape :: String -> String shellEscape s = "'" ++ concatMap escape s ++ "'" where diff --git a/libs/cassandra-util/cassandra-util.cabal b/libs/cassandra-util/cassandra-util.cabal index 927498c24f5..dc3e3abe855 100644 --- a/libs/cassandra-util/cassandra-util.cabal +++ b/libs/cassandra-util/cassandra-util.cabal @@ -76,6 +76,7 @@ library build-depends: aeson >=2.0.1.0 , base >=4.6 && <5.0 + , bytestring , conduit , cql >=3.0.0 , cql-io >=0.14 diff --git a/libs/cassandra-util/default.nix b/libs/cassandra-util/default.nix index e02d098a9b7..88dedc33fc7 100644 --- a/libs/cassandra-util/default.nix +++ b/libs/cassandra-util/default.nix @@ -5,6 +5,7 @@ { mkDerivation , aeson , base +, bytestring , conduit , cql , cql-io @@ -33,6 +34,7 @@ mkDerivation { libraryHaskellDepends = [ aeson base + bytestring conduit cql cql-io diff --git a/libs/cassandra-util/src/Cassandra/Settings.hs b/libs/cassandra-util/src/Cassandra/Settings.hs index 4e38328ef0b..eea824bccd9 100644 --- a/libs/cassandra-util/src/Cassandra/Settings.hs +++ b/libs/cassandra-util/src/Cassandra/Settings.hs @@ -25,17 +25,22 @@ module Cassandra.Settings initialContactsPlain, dcAwareRandomPolicy, dcFilterPolicyIfConfigured, + mkLogger, ) where import Control.Lens import Data.Aeson.Key qualified as Key import Data.Aeson.Lens +import Data.ByteString qualified as BS +import Data.ByteString.Builder +import Data.ByteString.Char8 qualified as BS8 +import Data.ByteString.Lazy qualified as LBS import Data.List.NonEmpty (NonEmpty (..)) import Data.List.NonEmpty qualified as NonEmpty import Data.Text (pack, stripSuffix, unpack) import Database.CQL.IO as C hiding (values) -import Database.CQL.IO.Tinylog as C (mkLogger) +import Database.CQL.IO.Tinylog qualified as CT import Imports import Network.Wreq import System.Logger qualified as Log @@ -90,3 +95,21 @@ dcAwareRandomPolicy dc = do where dcAcceptable :: Host -> IO Bool dcAcceptable host = pure $ (host ^. dataCentre) == dc + +mkLogger :: Maybe Text -> Log.Logger -> Logger +mkLogger mName logger = base {logMessage = suppressUseKeyspaceWarning} + where + base = CT.mkLogger (maybe logger (\n -> Log.clone (Just n) logger) mName) + isUseKeyspaceWarning msg = all (\needle -> needle `BS.isInfixOf` msg) useKeyspaceNeedles + suppressUseKeyspaceWarning level builder = do + let msg = LBS.toStrict $ toLazyByteString builder + case (level, isUseKeyspaceWarning msg) of + (LogWarn, True) -> pure () + _ -> logMessage base level builder + +-- This is a top-level constant to avoid repeated `pack` allocation on every log line +useKeyspaceNeedles :: [BS.ByteString] +useKeyspaceNeedles = + [ BS8.pack "non-qualified table names", + BS8.pack "Server warning: `USE `" + ] diff --git a/libs/cassandra-util/src/Cassandra/Util.hs b/libs/cassandra-util/src/Cassandra/Util.hs index d6968ead939..f876b6a41db 100644 --- a/libs/cassandra-util/src/Cassandra/Util.hs +++ b/libs/cassandra-util/src/Cassandra/Util.hs @@ -27,7 +27,8 @@ where import Cassandra.CQL import Cassandra.Options import Cassandra.Schema -import Cassandra.Settings (dcFilterPolicyIfConfigured, initialContactsDisco, initialContactsPlain, mkLogger) +import Cassandra.Settings (dcFilterPolicyIfConfigured, initialContactsDisco, initialContactsPlain) +import Cassandra.Settings qualified as CS import Data.Aeson import Data.Fixed import Data.List.NonEmpty qualified as NE @@ -36,7 +37,6 @@ import Data.Time (UTCTime, nominalDiffTimeToSeconds) import Data.Time.Clock (secondsToNominalDiffTime) import Data.Time.Clock.POSIX import Database.CQL.IO -import Database.CQL.IO.Tinylog qualified as CT import Imports hiding (init) import OpenSSL.Session qualified as OpenSSL import System.Logger qualified as Log @@ -44,7 +44,7 @@ import System.Logger qualified as Log defInitCassandra :: CassandraOpts -> Log.Logger -> IO ClientState defInitCassandra opts logger = do let basicCasSettings = - setLogger (CT.mkLogger logger) + setLogger (CS.mkLogger Nothing logger) . setPortNumber (fromIntegral opts.endpoint.port) . setContacts (unpack opts.endpoint.host) [] . setKeyspace (Keyspace opts.keyspace) @@ -67,7 +67,7 @@ initCassandraForService opts serviceName discoUrl mbSchemaVersion logger = do (initialContactsDisco ("cassandra_" ++ serviceName) . unpack) discoUrl let basicCasSettings = - setLogger (mkLogger (Log.clone (Just (pack ("cassandra." ++ serviceName))) logger)) + setLogger (CS.mkLogger (Just (pack ("cassandra." ++ serviceName))) logger) . setContacts (NE.head c) (NE.tail c) . setPortNumber (fromIntegral opts.endpoint.port) . setKeyspace (Keyspace opts.keyspace) diff --git a/libs/wire-api/default.nix b/libs/wire-api/default.nix index f0e9150c672..3108eaf3087 100644 --- a/libs/wire-api/default.nix +++ b/libs/wire-api/default.nix @@ -256,6 +256,7 @@ mkDerivation { hex hspec hspec-wai + http-api-data http-types imports iso3166-country-codes diff --git a/libs/wire-api/src/Wire/API/App.hs b/libs/wire-api/src/Wire/API/App.hs deleted file mode 100644 index 698f184667f..00000000000 --- a/libs/wire-api/src/Wire/API/App.hs +++ /dev/null @@ -1,185 +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 Wire.API.App where - -import Data.Aeson qualified as A -import Data.HashMap.Strict qualified as HM -import Data.Id -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, - category :: Category, - description :: Range 0 300 Text - } - deriving (A.FromJSON, A.ToJSON, S.ToSchema) via Schema GetApp - -newtype GetAppList = GetAppList {fromGetAppList :: [(UserId, GetApp)]} - deriving (A.FromJSON, A.ToJSON, S.ToSchema) via Schema GetAppList - -data PutApp = PutApp - { name :: Maybe Name, - assets :: Maybe [Asset], - accentId :: Maybe ColourId, - category :: Maybe Category, - description :: Maybe (Range 0 300 Text) - } - deriving (A.FromJSON, A.ToJSON, S.ToSchema) via Schema PutApp - -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" getAppObjectSchema - -getAppObjectSchema :: ObjectSchema SwaggerDoc GetApp -getAppObjectSchema = - 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 - <*> (.category) .= field "category" schema - <*> (.description) .= field "description" schema - -instance ToSchema GetAppList where - schema = GetAppList <$> fromGetAppList .= named "GetAppList" (array getAppWithIdSchema) - where - getAppWithIdSchema :: ValueSchema NamedSwaggerDoc (UserId, GetApp) - getAppWithIdSchema = - object "GetAppWithId" $ - (,) - <$> fst .= field "id" schema - <*> snd .= getAppObjectSchema - -instance ToSchema PutApp where - schema = - object "PutApp" $ - PutApp - <$> (.name) .= maybe_ (optField "name" schema) - <*> (.assets) .= maybe_ (optField "assets" (array schema)) - <*> (.accentId) .= maybe_ (optField "accent_id" schema) - <*> (.category) .= maybe_ (optField "category" schema) - <*> (.description) .= maybe_ (optField "description" schema) - -data CreatedApp = CreatedApp - { user :: User, - cookie :: SomeUserToken - } - deriving (A.FromJSON, A.ToJSON, S.ToSchema) via Schema CreatedApp - -instance ToSchema CreatedApp where - schema = - object "CreatedApp" $ - CreatedApp - <$> (.user) .= field "user" schema - <*> (.cookie) .= field "cookie" schema - -newtype RefreshAppCookieResponse = RefreshAppCookieResponse - {cookie :: SomeUserToken} - deriving (A.FromJSON, A.ToJSON, S.ToSchema) via Schema RefreshAppCookieResponse - -instance ToSchema RefreshAppCookieResponse where - schema = - object "RefreshAppCookieResponse" $ - RefreshAppCookieResponse <$> (.cookie) .= field "cookie" schema diff --git a/libs/wire-api/src/Wire/API/Conversation.hs b/libs/wire-api/src/Wire/API/Conversation.hs index 118346b5f9b..915fafd535e 100644 --- a/libs/wire-api/src/Wire/API/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Conversation.hs @@ -113,7 +113,7 @@ import Data.OpenApi qualified as S import Data.Qualified import Data.Range (Range, fromRange, rangedSchema) import Data.SOP -import Data.Schema +import Data.Schema as DS import Data.Set qualified as Set import Data.Singletons import Data.Text qualified as Text @@ -210,7 +210,7 @@ accessRolesSchemaOptV2 = toOutput .= accessRolesSchemaTuple `withParser` validat accessRolesSchemaTuple :: ObjectSchema SwaggerDoc (Maybe AccessRoleLegacy, Maybe (Set AccessRole)) accessRolesSchemaTuple = (,) - <$> fst .= optFieldWithDocModifier "access_role" (description ?~ "Deprecated, please use access_role_v2") (maybeWithDefault A.Null schema) + <$> fst .= optFieldWithDocModifier "access_role" (DS.description ?~ "Deprecated, please use access_role_v2") (maybeWithDefault A.Null schema) <*> snd .= optField "access_role_v2" (maybeWithDefault A.Null $ set schema) conversationMetadataObjectSchema :: @@ -222,7 +222,7 @@ conversationMetadataObjectSchema sch = <*> cnvmCreator .= optFieldWithDocModifier "creator" - (description ?~ "The creator's user ID") + (DS.description ?~ "The creator's user ID") (maybeWithDefault A.Null schema) <*> cnvmAccess .= field "access" (array schema) <*> cnvmAccessRoles .= sch @@ -234,7 +234,7 @@ conversationMetadataObjectSchema sch = <*> cnvmMessageTimer .= optFieldWithDocModifier "message_timer" - (description ?~ "Per-conversation message timer (can be null)") + (DS.description ?~ "Per-conversation message timer (can be null)") (maybeWithDefault A.Null schema) <*> cnvmReceiptMode .= optField "receipt_mode" (maybeWithDefault A.Null schema) <*> cnvmGroupConvType .= optField "group_conv_type" (maybeWithDefault A.Null schema) @@ -325,7 +325,7 @@ conversationSchema :: conversationSchema v = objectWithDocModifier ("OwnConversation" <> foldMap (Text.toUpper . versionText) v) - (description ?~ "A conversation object as returned from the server") + (DS.description ?~ "A conversation object as returned from the server") (ownConversationObjectSchema v) fromOwnConversation :: OwnConversation -> Conversation @@ -356,7 +356,7 @@ instance ToSchema Conversation where schema = objectWithDocModifier "Conversation" - (description ?~ "A conversation object as returned from the server") + (DS.description ?~ "A conversation object as returned from the server") $ conversationObjectSchema conversationObjectSchema :: ObjectSchema SwaggerDoc Conversation @@ -403,7 +403,7 @@ createGroupConversationSchema :: Maybe Version -> ValueSchema NamedSwaggerDoc Cr createGroupConversationSchema v = objectWithDocModifier "CreateGroupOwnConversation" - (description ?~ "A created group-conversation object extended with a list of failed-to-add users") + (DS.description ?~ "A created group-conversation object extended with a list of failed-to-add users") $ CreateGroupOwnConversation <$> cgcConversation .= ownConversationObjectSchema v <*> (toFlatList . cgcFailedToAdd) @@ -428,7 +428,7 @@ instance ToSchema CreateGroupConversation where schema = objectWithDocModifier "CreateGroupConversation" - (description ?~ "A created group-conversation object extended with a list of failed-to-add users") + (DS.description ?~ "A created group-conversation object extended with a list of failed-to-add users") $ CreateGroupConversation <$> (.conversation) .= conversationObjectSchema <*> (toFlatList . failedToAdd) .= field "failed_to_add" (fromFlatList <$> array schema) @@ -450,7 +450,7 @@ instance ToSchema ConversationCoverView where schema = objectWithDocModifier "ConversationCoverView" - (description ?~ "Limited view of Conversation.") + (DS.description ?~ "Limited view of Conversation.") $ ConversationCoverView <$> cnvCoverConvId .= field "id" schema <*> cnvCoverName .= optField "name" (maybeWithDefault A.Null schema) @@ -490,13 +490,13 @@ conversationListSchema :: conversationListSchema sch = objectWithDocModifier "ConversationList" - (description ?~ "Object holding a list of " <> convListItemName (Proxy @a)) + (DS.description ?~ "Object holding a list of " <> convListItemName (Proxy @a)) $ ConversationList <$> convList .= field "conversations" (array sch) <*> convHasMore .= fieldWithDocModifier "has_more" - (description ?~ "Indicator that the server has more conversations than returned") + (DS.description ?~ "Indicator that the server has more conversations than returned") schema type ConversationPagingName = "ConversationIds" @@ -529,7 +529,7 @@ instance ToSchema ListConversations where schema = objectWithDocModifier "ListConversations" - (description ?~ "A request to list some of a user's conversations, including remote ones. Maximum 1000 qualified conversation IDs") + (DS.description ?~ "A request to list some of a user's conversations, including remote ones. Maximum 1000 qualified conversation IDs") $ ListConversations <$> (fromRange . lcQualifiedIds) .= field "qualified_ids" (rangedSchema (array schema)) @@ -545,11 +545,11 @@ conversationsResponseSchema :: Maybe Version -> ValueSchema NamedSwaggerDoc ConversationsResponse conversationsResponseSchema v = - let notFoundDoc = description ?~ "These conversations either don't exist or are deleted." - failedDoc = description ?~ "The server failed to fetch these conversations, most likely due to network issues while contacting a remote server" + let notFoundDoc = DS.description ?~ "These conversations either don't exist or are deleted." + failedDoc = DS.description ?~ "The server failed to fetch these conversations, most likely due to network issues while contacting a remote server" in objectWithDocModifier ("ConversationsResponse" <> foldMap (Text.toUpper . versionText) v) - (description ?~ "Response object for getting metadata of a list of conversations") + (DS.description ?~ "Response object for getting metadata of a list of conversations") $ ConversationsResponse <$> crFound .= field "found" (array (conversationSchema v)) <*> crNotFound .= fieldWithDocModifier "not_found" notFoundDoc (array schema) @@ -580,7 +580,7 @@ data Access instance ToSchema Access where schema = - (S.schema . description ?~ "How users can join conversations") $ + (S.schema . DS.description ?~ "How users can join conversations") $ enum @Text "Access" $ mconcat [ element "private" PrivateAccess, @@ -725,7 +725,7 @@ toAccessRoleLegacy accessRoles = do instance ToSchema AccessRole where schema = - (S.schema . description ?~ desc) $ + (S.schema . DS.description ?~ desc) $ enum @Text "AccessRole" $ mconcat [ element "team_member" TeamMemberAccessRole, @@ -746,7 +746,7 @@ instance ToSchema AccessRole where instance ToSchema AccessRoleLegacy where schema = (S.schema . S.deprecated ?~ True) $ - (S.schema . description ?~ desc) $ + (S.schema . DS.description ?~ desc) $ enum @Text "AccessRoleLegacy" $ mconcat [ element "private" PrivateAccessRole, @@ -836,7 +836,7 @@ instance Default ReceiptMode where instance ToSchema ReceiptMode where schema = - (S.schema . description ?~ "Conversation receipt mode") $ + (S.schema . DS.description ?~ "Conversation receipt mode") $ ReceiptMode <$> unReceiptMode .= schema instance PostgresMarshall Int32 ReceiptMode where @@ -912,13 +912,13 @@ newConvSchema :: newConvSchema v sch = objectWithDocModifier ("NewConv" <> foldMap (Text.toUpper . versionText) v) - (description ?~ "JSON object to create a new conversation. When using 'qualified_users' (preferred), you can omit 'users'") + (DS.description ?~ "JSON object to create a new conversation. When using 'qualified_users' (preferred), you can omit 'users'") $ NewConv <$> newConvUsers .= ( fieldWithDocModifier "users" ( (S.deprecated ?~ True) - . (description ?~ usersDesc) + . (DS.description ?~ usersDesc) ) (array schema) <|> pure [] @@ -926,7 +926,7 @@ newConvSchema v sch = <*> newConvQualifiedUsers .= ( fieldWithDocModifier "qualified_users" - (description ?~ qualifiedUsersDesc) + (DS.description ?~ qualifiedUsersDesc) (array schema) <|> pure [] ) @@ -938,19 +938,19 @@ newConvSchema v sch = .= maybe_ ( optFieldWithDocModifier "team" - (description ?~ "Team information of this conversation") + (DS.description ?~ "Team information of this conversation") schema ) <*> newConvMessageTimer .= maybe_ ( optFieldWithDocModifier "message_timer" - (description ?~ "Per-conversation message timer") + (DS.description ?~ "Per-conversation message timer") schema ) <*> newConvReceiptMode .= maybe_ (optField "receipt_mode" schema) <*> newConvUsersRole - .= ( fieldWithDocModifier "conversation_role" (description ?~ usersRoleDesc) schema + .= ( fieldWithDocModifier "conversation_role" (DS.description ?~ usersRoleDesc) schema <|> pure roleNameWireAdmin ) <*> newConvProtocol @@ -961,19 +961,19 @@ newConvSchema v sch = <*> newConvCells .= (fromMaybe False <$> optField "cells" schema) <*> newConvChannelAddPermission .= maybe_ - (optFieldWithDocModifier "add_permission" (description ?~ "Channel add permission") schema) + (optFieldWithDocModifier "add_permission" (DS.description ?~ "Channel add permission") schema) <*> newConvSkipCreator .= ( fromMaybe False <$> optFieldWithDocModifier "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.") + (DS.description ?~ "Don't add creator to the conversation, only works for team admins not wanting to be part of the channels they create.") schema ) <*> newConvParent .= maybe_ ( optFieldWithDocModifier "parent" - (description ?~ "Parent conversation") + (DS.description ?~ "Parent conversation") schema ) <*> newConvHistory .= (fromMaybe def <$> optField "history" schema) @@ -1009,13 +1009,13 @@ instance ToSchema ConvTeamInfo where schema = objectWithDocModifier "ConvTeamInfo" - (description ?~ "Team information") + (DS.description ?~ "Team information") $ ConvTeamInfo <$> cnvTeamId .= field "teamid" schema <* const () .= fieldWithDocModifier "managed" - (description ?~ managedDesc) + (DS.description ?~ managedDesc) (c (False :: Bool)) where c :: (ToJSON a) => a -> ValueSchema SwaggerDoc () @@ -1037,13 +1037,13 @@ instance ToSchema NewOne2OneConv where schema = objectWithDocModifier "NewOne2OneConv" - (description ?~ "JSON object to create a new 1:1 conversation. When using 'qualified_users' (preferred), you can omit 'users'") + (DS.description ?~ "JSON object to create a new 1:1 conversation. When using 'qualified_users' (preferred), you can omit 'users'") $ NewOne2OneConv <$> (.users) .= ( fieldWithDocModifier "users" ( (S.deprecated ?~ True) - . (description ?~ usersDesc) + . (DS.description ?~ usersDesc) ) (array schema) <|> pure [] @@ -1051,16 +1051,16 @@ instance ToSchema NewOne2OneConv where <*> (.qualifiedUsers) .= ( fieldWithDocModifier "qualified_users" - (description ?~ qualifiedUsersDesc) + (DS.description ?~ qualifiedUsersDesc) (array schema) <|> pure [] ) - <*> name .= maybe_ (optField "name" schema) + <*> (.name) .= maybe_ (optField "name" schema) <*> (.team) .= maybe_ ( optFieldWithDocModifier "team" - (description ?~ "Team information of this conversation") + (DS.description ?~ "Team information of this conversation") schema ) where @@ -1140,7 +1140,7 @@ instance ToSchema ConversationRename where <$> cupName .= fieldWithDocModifier "name" - (description ?~ desc) + (DS.description ?~ desc) (unnamed (schema @Text)) where desc = "The new conversation name" @@ -1175,7 +1175,7 @@ data ConversationReceiptModeUpdate = ConversationReceiptModeUpdate instance ToSchema ConversationReceiptModeUpdate where schema = - objectWithDocModifier "ConversationReceiptModeUpdate" (description ?~ desc) $ + objectWithDocModifier "ConversationReceiptModeUpdate" (DS.description ?~ desc) $ ConversationReceiptModeUpdate <$> cruReceiptMode .= field "receipt_mode" (unnamed schema) where @@ -1196,7 +1196,7 @@ instance ToSchema ConversationMessageTimerUpdate where schema = objectWithDocModifier "ConversationMessageTimerUpdate" - (description ?~ "Contains conversation properties to update") + (DS.description ?~ "Contains conversation properties to update") $ ConversationMessageTimerUpdate <$> cupMessageTimer .= optField "message_timer" (maybeWithDefault A.Null schema) @@ -1229,7 +1229,7 @@ instance ToSchema ConversationJoin where schema = objectWithDocModifier "ConversationJoin" - (description ?~ "The action of some users joining a conversation") + (DS.description ?~ "The action of some users joining a conversation") $ ConversationJoin <$> (.users) .= field "users" (nonEmptyArray schema) <*> role .= field "role" schema @@ -1247,7 +1247,7 @@ instance ToSchema ConversationMemberUpdate where schema = objectWithDocModifier "ConversationMemberUpdate" - (description ?~ "The action of promoting/demoting a member of a conversation") + (DS.description ?~ "The action of promoting/demoting a member of a conversation") $ ConversationMemberUpdate <$> cmuTarget .= field "target" schema <*> cmuUpdate .= field "update" schema @@ -1264,7 +1264,7 @@ instance ToSchema ConversationRemoveMembers where schema = objectWithDocModifier "ConversationRemoveMembers" - (description ?~ "The action of removing members from a conversation") + (DS.description ?~ "The action of removing members from a conversation") $ ConversationRemoveMembers <$> crmTargets .= field "targets" (nonEmptyArray schema) <*> crmReason .= field "reason" schema @@ -1319,7 +1319,7 @@ instance ToSchema AddPermissionUpdate where schema = objectWithDocModifier "AddPermissionUpdate" - (description ?~ "The action of changing the permission to add members to a channel") + (DS.description ?~ "The action of changing the permission to add members to a channel") $ AddPermissionUpdate <$> addPermission .= field "add_permission" schema @@ -1337,7 +1337,7 @@ instance ToSchema ExtraConversationData where schema = objectWithDocModifier "ExtraConversationData" - (description ?~ "Extra conversation data, used for group conversations") + (DS.description ?~ "Extra conversation data, used for group conversations") $ ExtraConversationData <$> newGroupId .= optField "group_id" (maybeWithDefault A.Null schema) diff --git a/libs/wire-api/src/Wire/API/Error/Galley.hs b/libs/wire-api/src/Wire/API/Error/Galley.hs index f509888c780..0ce4976f735 100644 --- a/libs/wire-api/src/Wire/API/Error/Galley.hs +++ b/libs/wire-api/src/Wire/API/Error/Galley.hs @@ -666,12 +666,15 @@ instance ToSchema GroupInfoDiagnostics where <*> (.clients) .= field "clients" (array indexedClientSchema) <*> (.convId) .= convOrSubConvIdObjectSchema <*> (.domain) .= field "domain" schema + <* const (400 :: Int) .= field "code" schema + <* const ("inconsistent-group-state" :: Text) .= field "label" schema + <* const ("Submitted group info is inconsistent with the backend group state" :: Text) .= field "message" schema instance IsSwaggerError GroupInfoDiagnostics where addToOpenApi = addErrorResponseToSwagger (HTTP.statusCode groupInfoDiagnosticsStatus) $ mempty - & S.description .~ "Submitted group info is inconsistent with the backend group state" + & S.description .~ "Submitted group info is inconsistent with the backend group state (label `inconsistent-group-state`)" & S.content .~ singleton mediaType mediaTypeObject where mediaType = contentType $ Proxy @JSON diff --git a/libs/wire-api/src/Wire/API/Locale.hs b/libs/wire-api/src/Wire/API/Locale.hs index 576c7eeeb10..076f8b29d55 100644 --- a/libs/wire-api/src/Wire/API/Locale.hs +++ b/libs/wire-api/src/Wire/API/Locale.hs @@ -37,8 +37,9 @@ import Control.Applicative (optional) import Control.Error.Util (hush, note) import Data.Aeson (FromJSON, ToJSON) import Data.Attoparsec.Text +import Data.Default import Data.ISO3166_CountryCodes (CountryCode) -import Data.LanguageCodes (ISO639_1 (DE, FR)) +import Data.LanguageCodes (ISO639_1 (DE, EN, FR)) import Data.OpenApi qualified as S import Data.Schema import Data.Text qualified as Text @@ -145,6 +146,9 @@ data Locale = Locale deriving (Arbitrary) via (GenericUniform Locale) deriving (FromJSON, ToJSON, S.ToSchema) via Schema Locale +instance Default Locale where + def = Locale (Language EN) Nothing + instance ToSchema Locale where schema = locToText .= parsedText "Locale" (note err . parseLocale) where diff --git a/libs/wire-api/src/Wire/API/Routes/Features.hs b/libs/wire-api/src/Wire/API/Routes/Features.hs index 92037cc45fe..5759e37659e 100644 --- a/libs/wire-api/src/Wire/API/Routes/Features.hs +++ b/libs/wire-api/src/Wire/API/Routes/Features.hs @@ -36,4 +36,6 @@ type family FeatureErrors cfg where type family FeatureAPIDesc cfg where FeatureAPIDesc EnforceFileDownloadLocationConfig = "

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

" + FeatureAPIDesc RequireExternalEmailVerificationConfig = + "

Controls whether externally managed email addresses (from SAML or SCIM) must be verified by the user, or are auto-activated.

The external feature name is kept as validateSAMLemails for backward compatibility. That name is misleading because the feature also applies to SCIM-managed users, and it controls email ownership verification rather than generic email validation.

" FeatureAPIDesc _ = "" 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 3e35d7e06ce..4e7d5fdba14 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -42,7 +42,6 @@ import Network.Wai.Utilities import Servant (JSON) import Servant hiding (Handler, JSON, addHeader, respond) import Servant.OpenApi.Internal.Orphans () -import Wire.API.App import Wire.API.Call.Config (RTCConfiguration) import Wire.API.Connection hiding (MissingLegalholdConsent) import Wire.API.Deprecated @@ -1169,7 +1168,7 @@ type CreateAccessToken = ( Summary "Create a JWT DPoP access token" :> Description ( "Create an JWT DPoP access token for the client CSR, given a JWT DPoP proof, specified in the `DPoP` header. \ - \The access token will be returned as JWT DPoP token in the `DPoP` header." + \The access token will be returned in the JSON response body as a JWT DPoP token." ) :> ZLocalUser :> "clients" @@ -2128,17 +2127,17 @@ type AppsAPI = :> Capture "tid" TeamId :> "apps" :> Capture "uid" UserId - :> Get '[JSON] GetApp + :> Get '[JSON] UserProfile ) :<|> Named "get-apps" - ( Summary "Get all apps in a team" + ( Summary "Get all apps owned by the given team (not including collaborators)" :> From 'V15 :> ZLocalUser :> "teams" :> Capture "tid" TeamId :> "apps" - :> Get '[JSON] GetAppList + :> Get '[JSON] [UserProfile] ) :<|> Named "put-app" @@ -2162,5 +2161,6 @@ type AppsAPI = :> "apps" :> Capture "app" UserId :> "cookies" + :> ReqBody '[JSON] RefreshAppCookieRequest :> Post '[JSON] RefreshAppCookieResponse ) 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 b326f6e7715..2083e829754 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 @@ -42,7 +42,7 @@ type FeatureAPI = :<|> FeatureAPIGetPut SearchVisibilityAvailableConfig :<|> SearchVisibilityGet :<|> SearchVisibilitySet - :<|> FeatureAPIGet ValidateSAMLEmailsConfig + :<|> FeatureAPIGet RequireExternalEmailVerificationConfig :<|> FeatureAPIGet DigitalSignaturesConfig :<|> FeatureAPIGetPut AppLockConfig :<|> FeatureAPIGetPut FileSharingConfig @@ -108,7 +108,7 @@ type DeprecatedFeatureConfigs = [ LegalholdConfig, SSOConfig, SearchVisibilityAvailableConfig, - ValidateSAMLEmailsConfig, + RequireExternalEmailVerificationConfig, DigitalSignaturesConfig, AppLockConfig, FileSharingConfig, @@ -129,7 +129,7 @@ type family AllDeprecatedFeatureConfigAPI cfgs where type DeprecatedFeatureAPI = FeatureStatusDeprecatedGet DeprecationNotice1 SearchVisibilityAvailableConfig V2 :<|> FeatureStatusDeprecatedPut DeprecationNotice1 SearchVisibilityAvailableConfig V2 - :<|> FeatureStatusDeprecatedGet DeprecationNotice1 ValidateSAMLEmailsConfig V2 + :<|> FeatureStatusDeprecatedGet DeprecationNotice1 RequireExternalEmailVerificationConfig V2 :<|> FeatureStatusDeprecatedGet DeprecationNotice2 DigitalSignaturesConfig V2 type FeatureAPIGet cfg = diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Meetings.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Meetings.hs index 129685f6942..8a89b925412 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Meetings.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Meetings.hs @@ -73,3 +73,49 @@ type MeetingsAPI = :> CanThrow 'MeetingNotFound :> Get '[JSON] Meeting ) + :<|> Named + "list-meetings" + ( Summary "List all meetings for the authenticated user" + :> From 'V16 + :> ZLocalUser + :> "meetings" + :> "list" + :> Get '[JSON] [Meeting] + ) + :<|> Named + "add-meeting-invitation" + ( Summary "Add an email to the invited emails" + :> From 'V16 + :> ZLocalUser + :> "meetings" + :> Capture "domain" Domain + :> Capture "id" MeetingId + :> "invitations" + :> CanThrow 'MeetingNotFound + :> CanThrow 'AccessDenied + :> ReqBody '[JSON] MeetingEmailsInvitation + :> MultiVerb + 'POST + '[JSON] + '[RespondEmpty 200 "Invitation added"] + () + ) + :<|> Named + "remove-meeting-invitation" + ( Summary "Remove emails from the invited emails" + :> From 'V16 + :> ZLocalUser + :> "meetings" + :> Capture "domain" Domain + :> Capture "id" MeetingId + :> "invitations" + :> "delete" + :> CanThrow 'MeetingNotFound + :> CanThrow 'AccessDenied + :> ReqBody '[JSON] MeetingEmailsInvitation + :> MultiVerb + 'POST + '[JSON] + '[RespondEmpty 200 "Invitations removed"] + () + ) 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 c5b2aa7a6b4..ddf3e1c6b84 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs @@ -204,7 +204,15 @@ type IdpGetAll = Get '[JSON] IdPList -- | See also: 'validateNewIdP', 'idpCreate', 'idpCreateXML'. type IdpCreate = - ReqBodyCustomError '[RawXML, JSON] "wai-error" IdPMetadataInfo + Description + "Create a new identity provider.\n\ + \\n\ + \The `api_version` parameter controls the uniqueness constraint for IdP issuers:\n\ + \- `v1`: IdP issuers must be globally unique across the entire backend (all teams)\n\ + \- `v2` (default): IdP issuers must be unique per team (can be reused across different teams)\n\ + \\n\ + \These constraints apply to both, multi-ingress and standard backends." + :> ReqBodyCustomError '[RawXML, JSON] "wai-error" IdPMetadataInfo :> QueryParam' '[Optional, Strict] "replaces" SAML.IdPId :> QueryParam' '[Optional, Strict] "api_version" WireIdPAPIVersion -- see also: 'DeprecateSSOAPIV1' -- FUTUREWORK: The handle is restricted to 32 characters. Can we find a more reasonable upper bound and create a type for it? Also see `IdpUpdate`. diff --git a/libs/wire-api/src/Wire/API/Routes/Version.hs b/libs/wire-api/src/Wire/API/Routes/Version.hs index d54784f796b..064e66929e2 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 | V15 +data Version = V0 | V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 | V11 | V12 | V13 | V14 | V15 | V16 deriving stock (Eq, Ord, Bounded, Enum, Show, Generic) deriving (FromJSON, ToJSON) via (Schema Version) deriving (Arbitrary) via (GenericUniform Version) @@ -133,6 +133,8 @@ instance RenderableSymbol V14 where renderSymbol = "V14" instance RenderableSymbol V15 where renderSymbol = "V15" +instance RenderableSymbol V16 where renderSymbol = "V16" + -- | 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, @@ -156,6 +158,7 @@ versionInt V12 = 12 versionInt V13 = 13 versionInt V14 = 14 versionInt V15 = 15 +versionInt V16 = 16 supportedVersions :: [Version] supportedVersions = [minBound .. maxBound] @@ -278,7 +281,8 @@ isDevelopmentVersion V11 = False isDevelopmentVersion V12 = False isDevelopmentVersion V13 = False isDevelopmentVersion V14 = False -isDevelopmentVersion V15 = True +isDevelopmentVersion V15 = False +isDevelopmentVersion V16 = True developmentVersions :: [Version] developmentVersions = filter isDevelopmentVersion supportedVersions diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index e1bd98a718e..15117fde387 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -61,7 +61,7 @@ module Wire.API.Team.Feature SearchVisibilityAvailableConfig (..), SelfDeletingMessagesConfigB (..), SelfDeletingMessagesConfig, - ValidateSAMLEmailsConfig (..), + RequireExternalEmailVerificationConfig (..), DigitalSignaturesConfig (..), ConferenceCallingConfigB (..), ConferenceCallingConfig, @@ -256,7 +256,7 @@ data FeatureSingleton cfg where FeatureSingletonLegalholdConfig :: FeatureSingleton LegalholdConfig FeatureSingletonSSOConfig :: FeatureSingleton SSOConfig FeatureSingletonSearchVisibilityAvailableConfig :: FeatureSingleton SearchVisibilityAvailableConfig - FeatureSingletonValidateSAMLEmailsConfig :: FeatureSingleton ValidateSAMLEmailsConfig + FeatureSingletonRequireExternalEmailVerificationConfig :: FeatureSingleton RequireExternalEmailVerificationConfig FeatureSingletonDigitalSignaturesConfig :: FeatureSingleton DigitalSignaturesConfig FeatureSingletonConferenceCallingConfig :: FeatureSingleton ConferenceCallingConfig FeatureSingletonSndFactorPasswordChallengeConfig :: FeatureSingleton SndFactorPasswordChallengeConfig @@ -753,29 +753,35 @@ instance ToSchema SearchVisibilityAvailableConfig where type instance DeprecatedFeatureName V2 SearchVisibilityAvailableConfig = "search-visibility" -------------------------------------------------------------------------------- --- ValidateSAMLEmails feature +-- RequireExternalEmailVerification feature --- | This feature does not have a PUT endpoint. See Note [unsettable features]. -data ValidateSAMLEmailsConfig = ValidateSAMLEmailsConfig +-- | Controls whether externally managed email addresses (from SAML or SCIM) +-- must be verified by the user, or are auto-activated. When disabled, no +-- verification email is sent, but the address is still activated immediately +-- and can receive later account notifications such as new-device emails. +-- The external feature name is kept for backward compatibility. +-- +-- (This feature does not have a PUT endpoint. See Note [unsettable features].) +data RequireExternalEmailVerificationConfig = RequireExternalEmailVerificationConfig deriving (Eq, Show, Generic, GSOP.Generic) - deriving (Arbitrary) via (GenericUniform ValidateSAMLEmailsConfig) - deriving (RenderableSymbol) via (RenderableTypeName ValidateSAMLEmailsConfig) - deriving (ParseDbFeature, Default) via (TrivialFeature ValidateSAMLEmailsConfig) + deriving (Arbitrary) via (GenericUniform RequireExternalEmailVerificationConfig) + deriving (RenderableSymbol) via (RenderableTypeName RequireExternalEmailVerificationConfig) + deriving (ParseDbFeature, Default) via (TrivialFeature RequireExternalEmailVerificationConfig) -instance ToSchema ValidateSAMLEmailsConfig where - schema = object "ValidateSAMLEmailsConfig" objectSchema +instance ToSchema RequireExternalEmailVerificationConfig where + schema = object "RequireExternalEmailVerificationConfig" objectSchema -instance Default (LockableFeature ValidateSAMLEmailsConfig) where +instance Default (LockableFeature RequireExternalEmailVerificationConfig) where def = defUnlockedFeature -instance ToObjectSchema ValidateSAMLEmailsConfig where - objectSchema = pure ValidateSAMLEmailsConfig +instance ToObjectSchema RequireExternalEmailVerificationConfig where + objectSchema = pure RequireExternalEmailVerificationConfig -instance IsFeatureConfig ValidateSAMLEmailsConfig where - type FeatureSymbol ValidateSAMLEmailsConfig = "validateSAMLemails" - featureSingleton = FeatureSingletonValidateSAMLEmailsConfig +instance IsFeatureConfig RequireExternalEmailVerificationConfig where + type FeatureSymbol RequireExternalEmailVerificationConfig = "validateSAMLemails" + featureSingleton = FeatureSingletonRequireExternalEmailVerificationConfig -type instance DeprecatedFeatureName V2 ValidateSAMLEmailsConfig = "validate-saml-emails" +type instance DeprecatedFeatureName V2 RequireExternalEmailVerificationConfig = "validate-saml-emails" -------------------------------------------------------------------------------- -- DigitalSignatures feature @@ -2207,7 +2213,7 @@ type Features = SSOConfig, SearchVisibilityAvailableConfig, SearchVisibilityInboundConfig, - ValidateSAMLEmailsConfig, + RequireExternalEmailVerificationConfig, DigitalSignaturesConfig, AppLockConfig, FileSharingConfig, diff --git a/libs/wire-api/src/Wire/API/Team/FeatureFlags.hs b/libs/wire-api/src/Wire/API/Team/FeatureFlags.hs index e01490fb671..7915eb9a126 100644 --- a/libs/wire-api/src/Wire/API/Team/FeatureFlags.hs +++ b/libs/wire-api/src/Wire/API/Team/FeatureFlags.hs @@ -182,12 +182,20 @@ newtype instance FeatureDefaults SearchVisibilityInboundConfig deriving (FromJSON, ToJSON) via Defaults (Feature SearchVisibilityInboundConfig) deriving (ParseFeatureDefaults) via OptionalField SearchVisibilityInboundConfig -newtype instance FeatureDefaults ValidateSAMLEmailsConfig - = ValidateSAMLEmailsDefaults (Feature ValidateSAMLEmailsConfig) +newtype instance FeatureDefaults RequireExternalEmailVerificationConfig + = RequireExternalEmailVerificationDefaults (Feature RequireExternalEmailVerificationConfig) deriving stock (Eq, Show) deriving newtype (Default, GetFeatureDefaults) - deriving (FromJSON, ToJSON) via Defaults (Feature ValidateSAMLEmailsConfig) - deriving (ParseFeatureDefaults) via OptionalField ValidateSAMLEmailsConfig + deriving (FromJSON, ToJSON) via Defaults (Feature RequireExternalEmailVerificationConfig) + +instance ParseFeatureDefaults (FeatureDefaults RequireExternalEmailVerificationConfig) where + parseFeatureDefaults obj = + do + -- Accept the legacy typo in config input for backward compatibility, + -- but prefer the canonical feature key when both are present. + mCanonical :: Maybe (FeatureDefaults RequireExternalEmailVerificationConfig) <- obj .:? featureKey @RequireExternalEmailVerificationConfig + mLegacy :: Maybe (FeatureDefaults RequireExternalEmailVerificationConfig) <- obj .:? "validateSAMLEmails" + pure $ fromMaybe def (mCanonical <|> mLegacy) data instance FeatureDefaults DigitalSignaturesConfig = DigitalSignaturesDefaults deriving stock (Eq, Show) diff --git a/libs/wire-api/src/Wire/API/Team/Invitation.hs b/libs/wire-api/src/Wire/API/Team/Invitation.hs index 96d268c8258..b51e52a0eef 100644 --- a/libs/wire-api/src/Wire/API/Team/Invitation.hs +++ b/libs/wire-api/src/Wire/API/Team/Invitation.hs @@ -38,7 +38,7 @@ import Data.Json.Util import Data.Misc import Data.OpenApi qualified as S import Data.SOP -import Data.Schema +import Data.Schema as DS import Data.Text.Encoding qualified as TE import Imports import Servant (FromHttpApiData (..), ToHttpApiData (..)) @@ -74,19 +74,19 @@ instance ToSchema InvitationRequest where invitationRequestSchema :: Bool -> ValueSchema NamedSwaggerDoc InvitationRequest invitationRequestSchema allowExisting = - objectWithDocModifier "InvitationRequest" (description ?~ "A request to join a team on Wire.") $ + objectWithDocModifier "InvitationRequest" (DS.description ?~ "A request to join a team on Wire.") $ InvitationRequest <$> locale - .= optFieldWithDocModifier "locale" (description ?~ "Locale to use for the invitation.") (maybeWithDefault A.Null schema) + .= optFieldWithDocModifier "locale" (DS.description ?~ "Locale to use for the invitation.") (maybeWithDefault A.Null schema) <*> (.role) - .= optFieldWithDocModifier "role" (description ?~ "Role of the invitee (invited user).") (maybeWithDefault A.Null schema) + .= optFieldWithDocModifier "role" (DS.description ?~ "Role of the invitee (invited user).") (maybeWithDefault A.Null schema) <*> (.inviteeName) - .= optFieldWithDocModifier "name" (description ?~ "Name of the invitee (1 - 128 characters).") (maybeWithDefault A.Null schema) + .= optFieldWithDocModifier "name" (DS.description ?~ "Name of the invitee (1 - 128 characters).") (maybeWithDefault A.Null schema) <*> (.inviteeEmail) - .= fieldWithDocModifier "email" (description ?~ "Email of the invitee.") schema + .= fieldWithDocModifier "email" (DS.description ?~ "Email of the invitee.") schema <*> (.allowExisting) .= ( fromMaybe allowExisting - <$> optFieldWithDocModifier "allow_existing" (description ?~ "Whether invitations to existing users are allowed.") schema + <$> optFieldWithDocModifier "allow_existing" (DS.description ?~ "Whether invitations to existing users are allowed.") schema ) -------------------------------------------------------------------------------- @@ -112,29 +112,29 @@ instance ToSchema Invitation where schema = objectWithDocModifier "Invitation" - (description ?~ "An invitation to join a team on Wire. If invitee is invited from an existing personal account, inviter email is included.") + (DS.description ?~ "An invitation to join a team on Wire. If invitee is invited from an existing personal account, inviter email is included.") invitationObjectSchema invitationObjectSchema :: ObjectSchema SwaggerDoc Invitation invitationObjectSchema = Invitation <$> (.team) - .= fieldWithDocModifier "team" (description ?~ "Team ID of the inviting team") schema + .= fieldWithDocModifier "team" (DS.description ?~ "Team ID of the inviting team") schema <*> (.role) -- clients, when leaving "role" empty, can leave the default role choice to us - .= (fromMaybe defaultRole <$> optFieldWithDocModifier "role" (description ?~ "Role of the invited user") schema) + .= (fromMaybe defaultRole <$> optFieldWithDocModifier "role" (DS.description ?~ "Role of the invited user") schema) <*> (.invitationId) - .= fieldWithDocModifier "id" (description ?~ "UUID used to refer the invitation") schema + .= fieldWithDocModifier "id" (DS.description ?~ "UUID used to refer the invitation") schema <*> (.createdAt) - .= fieldWithDocModifier "created_at" (description ?~ "Timestamp of invitation creation") schema + .= fieldWithDocModifier "created_at" (DS.description ?~ "Timestamp of invitation creation") schema <*> (.createdBy) - .= optFieldWithDocModifier "created_by" (description ?~ "ID of the inviting user") (maybeWithDefault A.Null schema) + .= optFieldWithDocModifier "created_by" (DS.description ?~ "ID of the inviting user") (maybeWithDefault A.Null schema) <*> (.inviteeEmail) - .= fieldWithDocModifier "email" (description ?~ "Email of the invitee") schema + .= fieldWithDocModifier "email" (DS.description ?~ "Email of the invitee") schema <*> (.inviteeName) - .= optFieldWithDocModifier "name" (description ?~ "Name of the invitee (1 - 128 characters)") (maybeWithDefault A.Null schema) + .= optFieldWithDocModifier "name" (DS.description ?~ "Name of the invitee (1 - 128 characters)") (maybeWithDefault A.Null schema) <*> (fmap (TE.decodeUtf8 . serializeURIRef') . (.inviteeUrl)) - .= optFieldWithDocModifier "url" (description ?~ "URL of the invitation link to be sent to the invitee") (maybeWithDefault A.Null urlSchema) + .= optFieldWithDocModifier "url" (DS.description ?~ "URL of the invitation link to be sent to the invitee") (maybeWithDefault A.Null urlSchema) where urlSchema = parsedText "URIRef_Absolute" (runParser (uriParser strictURIParserOptions) . TE.encodeUtf8) @@ -191,10 +191,10 @@ data InvitationList = InvitationList instance ToSchema InvitationList where schema = - objectWithDocModifier "InvitationList" (description ?~ "A list of sent team invitations.") $ + objectWithDocModifier "InvitationList" (DS.description ?~ "A list of sent team invitations.") $ InvitationList <$> ilInvitations .= field "invitations" (array schema) - <*> ilHasMore .= fieldWithDocModifier "has_more" (description ?~ "Indicator that the server has more invitations than returned.") schema + <*> ilHasMore .= fieldWithDocModifier "has_more" (DS.description ?~ "Indicator that the server has more invitations than returned.") schema -------------------------------------------------------------------------------- -- AcceptTeamInvitation @@ -208,10 +208,10 @@ data AcceptTeamInvitation = AcceptTeamInvitation instance ToSchema AcceptTeamInvitation where schema = - objectWithDocModifier "AcceptTeamInvitation" (description ?~ "Accept an invitation to join a team on Wire.") $ + objectWithDocModifier "AcceptTeamInvitation" (DS.description ?~ "Accept an invitation to join a team on Wire.") $ AcceptTeamInvitation - <$> code .= fieldWithDocModifier "code" (description ?~ "Invitation code to accept.") schema - <*> password .= fieldWithDocModifier "password" (description ?~ "The user account password.") schema + <$> (.code) .= fieldWithDocModifier "code" (DS.description ?~ "Invitation code to accept.") schema + <*> (.password) .= fieldWithDocModifier "password" (DS.description ?~ "The user account password.") schema data InvitationUserView = InvitationUserView { invitation :: Invitation, diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 4800869efad..9435e0e4ad2 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -47,6 +47,15 @@ module Wire.API.User mkUserProfileWithEmail, userObjectSchema, + -- * Apps + NewApp (..), + AppInfo (..), + PutApp (..), + Category (..), + CreatedApp (..), + RefreshAppCookieRequest (..), + RefreshAppCookieResponse (..), + -- * UpgradePersonalToTeam CreateUserTeam (..), UpgradePersonalToTeamResponses, @@ -157,6 +166,7 @@ import Control.Arrow ((&&&)) import Control.Error.Safe (rightMay) import Control.Lens (makePrisms, over, view, (.~), (?~)) import Data.Aeson (FromJSON (..), ToJSON (..), withText) +import Data.Aeson qualified as A import Data.Aeson.Types qualified as A import Data.Attoparsec.ByteString qualified as Parser import Data.Bifunctor qualified as Bifunctor @@ -176,12 +186,12 @@ import Data.Id import Data.Json.Util (UTCTimeMillis, (#)) import Data.LegalHold (UserLegalHoldStatus) import Data.List.NonEmpty (NonEmpty (..)) -import Data.Misc (PlainTextPassword6, PlainTextPassword8) +import Data.Misc import Data.OpenApi qualified as S import Data.Qualified import Data.Range import Data.SOP -import Data.Schema +import Data.Schema hiding (description) import Data.Schema qualified as Schema import Data.Set qualified as Set import Data.Text qualified as T @@ -212,12 +222,12 @@ import Wire.API.Team.Member (TeamMember) import Wire.API.Team.Member qualified as TeamMember import Wire.API.Team.Role import Wire.API.User.Activation (ActivationCode, ActivationKey) -import Wire.API.User.Auth (CookieLabel) +import Wire.API.User.Auth import Wire.API.User.Identity hiding (toByteString) import Wire.API.User.Password import Wire.API.User.Profile import Wire.API.User.RichInfo -import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) +import Wire.Arbitrary as Arbitrary -------------------------------------------------------------------------------- -- UserIdList @@ -525,6 +535,7 @@ data UserProfile = UserProfile profileLegalholdStatus :: UserLegalHoldStatus, profileSupportedProtocols :: Set BaseProtocolTag, profileType :: UserType, + profileApp :: Maybe AppInfo, profileSearchable :: Bool } deriving stock (Eq, Show, Generic) @@ -532,40 +543,43 @@ data UserProfile = UserProfile deriving (FromJSON, ToJSON, S.ToSchema) via (Schema UserProfile) instance ToSchema UserProfile where - schema = - object "UserProfile" $ - UserProfile - <$> profileQualifiedId - .= field "qualified_id" schema - <* (qUnqualified . profileQualifiedId) - .= optional (field "id" (deprecatedSchema "qualified_id" schema)) - <*> profileName - .= field "name" schema - <*> profileTextStatus - .= maybe_ (optField "text_status" schema) - <*> profilePict - .= (field "picture" schema <|> pure noPict) - <*> profileAssets - .= (field "assets" (array schema) <|> pure []) - <*> profileAccentId - .= field "accent_id" schema - <*> ((\del -> if del then Just True else Nothing) . profileDeleted) - .= maybe_ (fromMaybe False <$> optField "deleted" schema) - <*> profileService - .= maybe_ (optField "service" schema) - <*> profileHandle - .= maybe_ (optField "handle" schema) - <*> profileExpire - .= maybe_ (optField "expires_at" schema) - <*> profileTeam - .= maybe_ (optField "team" schema) - <*> profileEmail - .= maybe_ (optField "email" schema) - <*> profileLegalholdStatus - .= field "legalhold_status" schema - <*> profileSupportedProtocols .= supportedProtocolsObjectSchema - <*> profileType .= fmap (fromMaybe UserTypeRegular) (optField "type" schema) - <*> profileSearchable .= fmap (fromMaybe True) (optField "searchable" schema) + schema = object "UserProfile" userProfileObjectSchema + +userProfileObjectSchema :: ObjectSchema SwaggerDoc UserProfile +userProfileObjectSchema = + UserProfile + <$> profileQualifiedId + .= field "qualified_id" schema + <* (qUnqualified . profileQualifiedId) + .= optional (field "id" (deprecatedSchema "qualified_id" schema)) + <*> profileName + .= field "name" schema + <*> profileTextStatus + .= maybe_ (optField "text_status" schema) + <*> profilePict + .= (field "picture" schema <|> pure noPict) + <*> profileAssets + .= (field "assets" (array schema) <|> pure []) + <*> profileAccentId + .= field "accent_id" schema + <*> ((\del -> if del then Just True else Nothing) . profileDeleted) + .= maybe_ (fromMaybe False <$> optField "deleted" schema) + <*> profileService + .= maybe_ (optField "service" schema) + <*> profileHandle + .= maybe_ (optField "handle" schema) + <*> profileExpire + .= maybe_ (optField "expires_at" schema) + <*> profileTeam + .= maybe_ (optField "team" schema) + <*> profileEmail + .= maybe_ (optField "email" schema) + <*> profileLegalholdStatus + .= field "legalhold_status" schema + <*> profileSupportedProtocols .= supportedProtocolsObjectSchema + <*> profileType .= fmap (fromMaybe UserTypeRegular) (optField "type" schema) + <*> profileApp .= maybe_ (optField "app" schema) + <*> profileSearchable .= fmap (fromMaybe True) (optField "searchable" schema) -------------------------------------------------------------------------------- -- SelfProfile @@ -732,9 +746,9 @@ instance FromJSON (EmailVisibility ()) where "visible_to_self" -> pure EmailVisibleToSelf _ -> fail "unexpected value for EmailVisibility settings" --- | Create profile, overwriting the email field. Called `mkUserProfile`. -mkUserProfileWithEmail :: Maybe EmailAddress -> UserType -> User -> UserLegalHoldStatus -> UserProfile -mkUserProfileWithEmail memail userType u legalHoldStatus = +-- | Create profile, overwriting the email field. Called by `mkUserProfile`. +mkUserProfileWithEmail :: Maybe EmailAddress -> User -> Maybe AppInfo -> UserLegalHoldStatus -> UserProfile +mkUserProfileWithEmail memail u mba legalHoldStatus = -- This profile would be visible to any other user. When a new field is -- added, please make sure it is OK for other users to have access to it. UserProfile @@ -752,12 +766,13 @@ mkUserProfileWithEmail memail userType u legalHoldStatus = profileEmail = memail, profileLegalholdStatus = legalHoldStatus, profileSupportedProtocols = userSupportedProtocols u, - profileType = userType, + profileType = u.userType, + profileApp = mba, profileSearchable = userSearchable u } -mkUserProfile :: EmailVisibilityConfigWithViewer -> UserType -> User -> UserLegalHoldStatus -> UserProfile -mkUserProfile emailVisibilityConfigAndViewer userType u legalHoldStatus = +mkUserProfile :: EmailVisibilityConfigWithViewer -> User -> Maybe AppInfo -> UserLegalHoldStatus -> UserProfile +mkUserProfile emailVisibilityConfigAndViewer u mba legalHoldStatus = let isEmailVisible = case emailVisibilityConfigAndViewer of EmailVisibleToSelf -> False EmailVisibleIfOnTeam -> isJust (userTeam u) @@ -765,7 +780,7 @@ mkUserProfile emailVisibilityConfigAndViewer userType u legalHoldStatus = EmailVisibleIfOnSameTeam (Just (viewerTeamId, viewerMembership)) -> Just viewerTeamId == userTeam u && TeamMember.hasPermission viewerMembership TeamMember.ViewSameTeamEmails - in mkUserProfileWithEmail (if isEmailVisible then userEmail u else Nothing) userType u legalHoldStatus + in mkUserProfileWithEmail (if isEmailVisible then userEmail u else Nothing) u mba legalHoldStatus -------------------------------------------------------------------------------- -- NewUser @@ -1475,7 +1490,7 @@ instance ToSchema PasswordChange where schema = over doc - ( description + ( Schema.description ?~ "Data to change a password. The old password is required if \ \a password already exists." ) @@ -1735,12 +1750,12 @@ data VerifyDeleteUser = VerifyDeleteUser instance ToSchema VerifyDeleteUser where schema = - objectWithDocModifier "VerifyDeleteUser" (description ?~ "Data for verifying an account deletion.") $ + objectWithDocModifier "VerifyDeleteUser" (Schema.description ?~ "Data for verifying an account deletion.") $ VerifyDeleteUser <$> verifyDeleteUserKey - .= fieldWithDocModifier "key" (description ?~ "The identifying key of the account (i.e. user ID).") schema + .= fieldWithDocModifier "key" (Schema.description ?~ "The identifying key of the account (i.e. user ID).") schema <*> verifyDeleteUserCode - .= fieldWithDocModifier "code" (description ?~ "The verification code.") schema + .= fieldWithDocModifier "code" (Schema.description ?~ "The verification code.") schema -- | A response for a pending deletion code. newtype DeletionCodeTimeout = DeletionCodeTimeout @@ -2070,3 +2085,119 @@ instance ToSchema ListUsersById where ListUsersById <$> listUsersByIdFound .= field "found" (array schema) <*> listUsersByIdFailed .= maybe_ (optField "failed" $ nonEmptyArray schema) + +-------------------------------------------------------------------------------- +-- Apps (can't easily go into its own module because cyclical deps) + +data NewApp = NewApp + { name :: Name, + assets :: [Asset], + accentId :: ColourId, + category :: Category, + description :: Range 0 300 Text, + -- | admin password for additional access control + password :: PlainTextPassword6 + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform NewApp) + deriving (A.FromJSON, A.ToJSON, S.ToSchema) via Schema NewApp + +data AppInfo = AppInfo + { category :: Category, + description :: Range 0 300 Text + } + deriving (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform AppInfo) + deriving (A.FromJSON, A.ToJSON, S.ToSchema) via Schema AppInfo + +data PutApp = PutApp + { name :: Maybe Name, + assets :: Maybe [Asset], + accentId :: Maybe ColourId, + category :: Maybe Category, + description :: Maybe (Range 0 300 Text) + } + deriving (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform PutApp) + deriving (A.FromJSON, A.ToJSON, S.ToSchema) via Schema PutApp + +newtype Category = Category {fromCategory :: Text} + deriving (Eq, Ord, Show, Read, Generic) + deriving (Arbitrary) via GenericUniform Category + deriving (A.FromJSON, A.ToJSON, S.ToSchema) via (Schema Category) + +instance ToSchema Category where + schema = over doc desc (Category <$> fromCategory .= schema @Text) + where + desc = S.description ?~ "Category name (if uncertain, pick \"other\")" + +instance ToSchema NewApp where + schema = + object "NewApp" $ + NewApp + <$> (.name) .= field "name" schema + <*> (.assets) .= (fromMaybe [] <$> optField "assets" (array schema)) + <*> (.accentId) .= (fromMaybe defaultAccentId <$> optField "accent_id" schema) + <*> (.category) .= field "category" schema + <*> (.description) .= field "description" schema + <*> (.password) .= field "password" schema + +instance ToSchema AppInfo where + schema = object "AppInfo" appInfoObjectSchema + +appInfoObjectSchema :: ObjectSchema SwaggerDoc AppInfo +appInfoObjectSchema = + AppInfo + <$> (.category) .= field "category" schema + <*> (.description) .= field "description" schema + +instance ToSchema PutApp where + schema = + object "PutApp" $ + PutApp + <$> (.name) .= maybe_ (optField "name" schema) + <*> (.assets) .= maybe_ (optField "assets" (array schema)) + <*> (.accentId) .= maybe_ (optField "accent_id" schema) + <*> (.category) .= maybe_ (optField "category" schema) + <*> (.description) .= maybe_ (optField "description" schema) + +data CreatedApp = CreatedApp + { user :: UserProfile, + cookie :: SomeUserToken + } + deriving stock (Eq, Show, Generic) + deriving (A.FromJSON, A.ToJSON, S.ToSchema) via Schema CreatedApp + +instance ToSchema CreatedApp where + schema = + object "CreatedApp" $ + CreatedApp + <$> (.user) .= field "user" schema + <*> (.cookie) .= field "cookie" schema + +newtype RefreshAppCookieRequest = RefreshAppCookieRequest + { password :: Maybe PlainTextPassword6 + } + deriving (A.FromJSON, A.ToJSON, S.ToSchema) via Schema RefreshAppCookieRequest + +instance ToSchema RefreshAppCookieRequest where + schema = + object "RefreshAppCookieRequest" $ + RefreshAppCookieRequest + <$> (.password) + .= optFieldWithDocModifier + "password" + ( S.description + ?~ "The password of the authenticated admin for verification. \ + \or if the user has only SAML credentials." + ) + (maybeWithDefault A.Null schema) + +newtype RefreshAppCookieResponse = RefreshAppCookieResponse + {cookie :: SomeUserToken} + deriving (A.FromJSON, A.ToJSON, S.ToSchema) via Schema RefreshAppCookieResponse + +instance ToSchema RefreshAppCookieResponse where + schema = + object "RefreshAppCookieResponse" $ + RefreshAppCookieResponse <$> (.cookie) .= field "cookie" schema diff --git a/libs/wire-api/src/Wire/API/User/Auth.hs b/libs/wire-api/src/Wire/API/User/Auth.hs index 202347cc301..7d592bdf2b9 100644 --- a/libs/wire-api/src/Wire/API/User/Auth.hs +++ b/libs/wire-api/src/Wire/API/User/Auth.hs @@ -482,7 +482,7 @@ instance AsHeaders '[Maybe UserTokenCookie] AccessToken SomeAccess where data SomeUserToken = PlainUserToken (ZAuth.Token ZAuth.U) | LHUserToken (ZAuth.Token ZAuth.LU) - deriving (Show) + deriving (Eq, Show) instance ToSchema SomeUserToken where schema = diff --git a/libs/wire-api/src/Wire/API/User/Client.hs b/libs/wire-api/src/Wire/API/User/Client.hs index 50986d499d6..0e490f0ff3c 100644 --- a/libs/wire-api/src/Wire/API/User/Client.hs +++ b/libs/wire-api/src/Wire/API/User/Client.hs @@ -906,6 +906,7 @@ instance ToSchema RmClient where "password" ( description ?~ "The password of the authenticated user for verification. \ - \The password is not required for deleting temporary clients." + \The password is not required for deleting temporary clients. \ + \or if the user has only SAML credentials." ) (maybeWithDefault A.Null schema) diff --git a/libs/wire-api/src/Wire/API/User/Profile.hs b/libs/wire-api/src/Wire/API/User/Profile.hs index d3634799df6..372e980cabf 100644 --- a/libs/wire-api/src/Wire/API/User/Profile.hs +++ b/libs/wire-api/src/Wire/API/User/Profile.hs @@ -284,7 +284,7 @@ newtype Pict = Pict {fromPict :: [A.Object]} instance ToSchema Pict where schema = - named "Pict" $ + named "Pict_DEPRECATED_USE_ASSETS_INSTEAD" $ Pict <$> fromPict .= untypedRangedSchema 0 10 (array jsonObject) instance Arbitrary Pict where diff --git a/libs/wire-api/src/Wire/API/User/Search.hs b/libs/wire-api/src/Wire/API/User/Search.hs index cb9287a18ea..25ac61f78d6 100644 --- a/libs/wire-api/src/Wire/API/User/Search.hs +++ b/libs/wire-api/src/Wire/API/User/Search.hs @@ -190,6 +190,7 @@ instance ToSchema Sso where -- | Returned by 'browseTeam' under @/teams/:tid/search@. data TeamContact = TeamContact { teamContactUserId :: UserId, + teamContactUserType :: UserType, teamContactName :: Text, teamContactColorId :: Maybe Int, teamContactHandle :: Maybe Text, @@ -214,6 +215,7 @@ instance ToSchema TeamContact where object "TeamContact" $ TeamContact <$> teamContactUserId .= field "id" schema + <*> teamContactUserType .= field "type" schema <*> teamContactName .= field "name" schema <*> teamContactColorId .= optField "accent_id" (maybeWithDefault Aeson.Null schema) <*> teamContactHandle .= optField "handle" (maybeWithDefault Aeson.Null schema) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Feature_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Feature_team.hs index 540fa355c3f..63ca21f3541 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Feature_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Feature_team.hs @@ -56,8 +56,8 @@ testObject_Feature_team_10 = Feature FeatureStatusDisabled SSOConfig testObject_Feature_team_11 :: Feature SearchVisibilityAvailableConfig testObject_Feature_team_11 = Feature FeatureStatusEnabled SearchVisibilityAvailableConfig -testObject_Feature_team_12 :: Feature ValidateSAMLEmailsConfig -testObject_Feature_team_12 = Feature FeatureStatusDisabled ValidateSAMLEmailsConfig +testObject_Feature_team_12 :: Feature RequireExternalEmailVerificationConfig +testObject_Feature_team_12 = Feature FeatureStatusDisabled RequireExternalEmailVerificationConfig testObject_Feature_team_13 :: Feature DigitalSignaturesConfig testObject_Feature_team_13 = Feature FeatureStatusEnabled DigitalSignaturesConfig diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LockableFeaturePatch_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LockableFeaturePatch_team.hs index 478398eb383..b8da4386055 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LockableFeaturePatch_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LockableFeaturePatch_team.hs @@ -56,8 +56,8 @@ testObject_LockableFeaturePatch_team_10 = LockableFeaturePatch (Just FeatureStat testObject_LockableFeaturePatch_team_11 :: LockableFeaturePatch SearchVisibilityAvailableConfig testObject_LockableFeaturePatch_team_11 = LockableFeaturePatch (Just FeatureStatusEnabled) (Just LockStatusLocked) (Just SearchVisibilityAvailableConfig) -testObject_LockableFeaturePatch_team_12 :: LockableFeaturePatch ValidateSAMLEmailsConfig -testObject_LockableFeaturePatch_team_12 = LockableFeaturePatch (Just FeatureStatusDisabled) Nothing (Just ValidateSAMLEmailsConfig) +testObject_LockableFeaturePatch_team_12 :: LockableFeaturePatch RequireExternalEmailVerificationConfig +testObject_LockableFeaturePatch_team_12 = LockableFeaturePatch (Just FeatureStatusDisabled) Nothing (Just RequireExternalEmailVerificationConfig) testObject_LockableFeaturePatch_team_13 :: LockableFeaturePatch DigitalSignaturesConfig testObject_LockableFeaturePatch_team_13 = LockableFeaturePatch (Just FeatureStatusEnabled) (Just LockStatusLocked) (Just DigitalSignaturesConfig) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LockableFeature_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LockableFeature_team.hs index 2cfb3a4cdbd..b6e17ed1334 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LockableFeature_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LockableFeature_team.hs @@ -58,8 +58,8 @@ testObject_LockableFeature_team_10 = LockableFeature FeatureStatusDisabled LockS testObject_LockableFeature_team_11 :: LockableFeature SearchVisibilityAvailableConfig testObject_LockableFeature_team_11 = LockableFeature FeatureStatusEnabled LockStatusLocked SearchVisibilityAvailableConfig -testObject_LockableFeature_team_12 :: LockableFeature ValidateSAMLEmailsConfig -testObject_LockableFeature_team_12 = LockableFeature FeatureStatusDisabled LockStatusLocked ValidateSAMLEmailsConfig +testObject_LockableFeature_team_12 :: LockableFeature RequireExternalEmailVerificationConfig +testObject_LockableFeature_team_12 = LockableFeature FeatureStatusDisabled LockStatusLocked RequireExternalEmailVerificationConfig testObject_LockableFeature_team_13 :: LockableFeature DigitalSignaturesConfig testObject_LockableFeature_team_13 = LockableFeature FeatureStatusEnabled LockStatusLocked DigitalSignaturesConfig diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SearchResult_20TeamContact_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SearchResult_20TeamContact_user.hs index b2bef3d4fb5..bd4fb4d2b8b 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SearchResult_20TeamContact_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SearchResult_20TeamContact_user.hs @@ -31,6 +31,7 @@ teamContactTemplate :: TeamContact teamContactTemplate = TeamContact { teamContactUserId = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000")), + teamContactUserType = UserTypeRegular, teamContactName = "", teamContactColorId = Nothing, teamContactHandle = Nothing, @@ -56,6 +57,7 @@ testObject_SearchResult_20TeamContact_user_1 = searchResults = [ teamContactTemplate { teamContactUserId = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000")), + teamContactUserType = UserTypeApp, teamContactColorId = Just 0, teamContactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001"))), teamContactEmail = Just (unsafeEmailAddress "some" "example"), @@ -70,6 +72,7 @@ testObject_SearchResult_20TeamContact_user_1 = }, teamContactTemplate { teamContactUserId = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000000")), + teamContactUserType = UserTypeRegular, teamContactColorId = Just 0, teamContactHandle = Just "", teamContactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001"))), diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/TeamContact_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/TeamContact_user.hs index fd50b57760d..4c2fd94b36e 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/TeamContact_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/TeamContact_user.hs @@ -30,6 +30,7 @@ teamContactTemplate :: TeamContact teamContactTemplate = TeamContact { teamContactUserId = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000200000000")), + teamContactUserType = UserTypeRegular, teamContactName = "", teamContactColorId = Nothing, teamContactHandle = Nothing, diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/UserProfile_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/UserProfile_user.hs index 5f47e25dff8..6633d2a9e42 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/UserProfile_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/UserProfile_user.hs @@ -25,6 +25,7 @@ import Data.Id import Data.Json.Util import Data.LegalHold import Data.Qualified +import Data.Range import Data.UUID qualified as UUID import Imports import Wire.API.Provider.Service @@ -52,6 +53,7 @@ testObject_UserProfile_user_1 = profileLegalholdStatus = UserLegalHoldDisabled, profileSupportedProtocols = defSupportedProtocols, profileType = UserTypeRegular, + profileApp = Nothing, profileSearchable = True } @@ -84,5 +86,11 @@ testObject_UserProfile_user_2 = profileLegalholdStatus = UserLegalHoldNoConsent, profileSupportedProtocols = defSupportedProtocols, profileType = UserTypeApp, + profileApp = + Just $ + AppInfo + { category = Category "other", + description = unsafeRange "bloob" + }, profileSearchable = True } 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 b27cf7a1ad2..de8aaafc9fb 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 @@ -21,6 +21,7 @@ import Imports import Test.Tasty import Test.Tasty.HUnit import Test.Wire.API.Golden.Manual.Activate_user +import Test.Wire.API.Golden.Manual.App import Test.Wire.API.Golden.Manual.CannonId import Test.Wire.API.Golden.Manual.ClientCapability import Test.Wire.API.Golden.Manual.ClientCapabilityList @@ -70,7 +71,18 @@ tests :: TestTree tests = testGroup "Manual golden tests" - [ testGroup "UserGroupPage" $ + [ testGroup "NewApp" $ + testObjects [(testObject_NewApp_1, "testObject_NewApp_1.json")], + testGroup "CreatedApp" $ + testObjects [(testObject_CreatedApp_1, "testObject_CreatedApp_1.json")], + testGroup "AppInfo" $ + testObjects [(testObject_AppInfo_1, "testObject_AppInfo_1.json")], + testGroup "PutApp" $ + testObjects + [ (testObject_PutApp_1, "testObject_PutApp_1.json"), + (testObject_PutApp_2, "testObject_PutApp_2.json") + ], + testGroup "UserGroupPage" $ testObjects [ (testObject_UserGroupPage_1, "testObject_UserGroupPage_1.json"), (testObject_UserGroupPage_2, "testObject_UserGroupPage_2.json"), diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/App.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/App.hs new file mode 100644 index 00000000000..3544ae22ad8 --- /dev/null +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/App.hs @@ -0,0 +1,65 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2026 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.Wire.API.Golden.Manual.App where + +import Data.Misc +import Data.Range +import Imports +import Test.Wire.API.Golden.Generated.UserProfile_user +import Web.HttpApiData +import Wire.API.User +import Wire.API.User.Auth (SomeUserToken) + +someToken :: SomeUserToken +someToken = either undefined id $ parseUrlPiece "DTHdPvHSFolvyGVvuaexZ9DKptwnxTSn8UhKc-6A9q34s4q0YY3_CgpYxDMr56crHrW79EPwKu2BLwQkFT7wBw==.v=1.k=1.d=1773661988.t=u.l=.u=ac638199-8816-439f-88dd-8e206c9b5baa.r=fa16d9df" + +testObject_NewApp_1 :: NewApp +testObject_NewApp_1 = + NewApp + (either undefined id $ mkName "good name") + mempty + defaultAccentId + (Category "other") + (unsafeRange "good description") + (plainTextPassword6Unsafe "good password") + +testObject_CreatedApp_1 :: CreatedApp +testObject_CreatedApp_1 = + CreatedApp testObject_UserProfile_user_2 someToken + +testObject_AppInfo_1 :: AppInfo +testObject_AppInfo_1 = + AppInfo (Category "other") (unsafeRange "good description") + +testObject_PutApp_1 :: PutApp +testObject_PutApp_1 = + PutApp + (Just (either undefined id $ mkName "good name")) + (Just mempty) + (Just defaultAccentId) + (Just (Category "other")) + (Just (unsafeRange "good description")) + +testObject_PutApp_2 :: PutApp +testObject_PutApp_2 = + PutApp + Nothing + Nothing + Nothing + Nothing + Nothing diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ListUsersById.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ListUsersById.hs index 34e78a8f400..34e63641db6 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ListUsersById.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ListUsersById.hs @@ -23,6 +23,7 @@ import Data.Domain import Data.Id import Data.LegalHold import Data.Qualified +import Data.Range import Data.Set qualified as Set import Data.UUID qualified as UUID import Imports @@ -54,6 +55,7 @@ profile1 = profileLegalholdStatus = UserLegalHoldDisabled, profileSupportedProtocols = defSupportedProtocols, profileType = UserTypeRegular, + profileApp = Nothing, profileSearchable = True } profile2 = @@ -73,6 +75,12 @@ profile2 = profileLegalholdStatus = UserLegalHoldDisabled, profileSupportedProtocols = Set.fromList [BaseProtocolProteusTag, BaseProtocolMLSTag], profileType = UserTypeRegular, + profileApp = + Just $ + AppInfo + { category = Category "other", + description = unsafeRange "bloob" + }, profileSearchable = True } diff --git a/libs/wire-api/test/golden/gentests.sh b/libs/wire-api/test/golden/gentests.sh index a9ec9c3c8e2..e0ef4f5cadf 100644 --- a/libs/wire-api/test/golden/gentests.sh +++ b/libs/wire-api/test/golden/gentests.sh @@ -17,7 +17,7 @@ export GOLDEN_TESTDIR="test/unit/Test/Wire/API/Golden/Generated" # trap cleanup EXIT function cleanup() { - [ -z "$GOLDEN_TMPDIR" ] || rm -rf "$GOLDEN_TMPDIR" + [[ -z "$GOLDEN_TMPDIR" ]] || rm -rf "$GOLDEN_TMPDIR" } gen_imports() { diff --git a/libs/wire-api/test/golden/testObject_AppInfo_1.json b/libs/wire-api/test/golden/testObject_AppInfo_1.json new file mode 100644 index 00000000000..59846475013 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_AppInfo_1.json @@ -0,0 +1,4 @@ +{ + "category": "other", + "description": "good description" +} diff --git a/libs/wire-api/test/golden/testObject_ConvIdsPage_1.json b/libs/wire-api/test/golden/testObject_ConvIdsPage_1.json deleted file mode 100644 index 458d7929648..00000000000 --- a/libs/wire-api/test/golden/testObject_ConvIdsPage_1.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "has_more": false, - "paging_state": "AA==", - "qualified_conversations": [] -} diff --git a/libs/wire-api/test/golden/testObject_ConvIdsPage_2.json b/libs/wire-api/test/golden/testObject_ConvIdsPage_2.json deleted file mode 100644 index 36536b222b7..00000000000 --- a/libs/wire-api/test/golden/testObject_ConvIdsPage_2.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "has_more": true, - "paging_state": "AA==", - "qualified_conversations": [ - { - "domain": "domain.example.com", - "id": "00000018-0000-0020-0000-000e00000002" - } - ] -} diff --git a/libs/wire-api/test/golden/testObject_CreateScimTokenResponse_1.json b/libs/wire-api/test/golden/testObject_CreateScimTokenResponse_1.json index 3896abc8201..c60bdf563d2 100644 --- a/libs/wire-api/test/golden/testObject_CreateScimTokenResponse_1.json +++ b/libs/wire-api/test/golden/testObject_CreateScimTokenResponse_1.json @@ -3,8 +3,8 @@ "created_at": "2024-10-22T18:04:50Z", "description": "description", "id": "e25faea1-ee2d-4fd8-bf25-e6748d392b23", - "team": "2853751e-9fb6-4425-b1bd-bd8aa2640c69", - "name": "token name" + "name": "token name", + "team": "2853751e-9fb6-4425-b1bd-bd8aa2640c69" }, "token": "token" } diff --git a/libs/wire-api/test/golden/testObject_CreateScimToken_4.json b/libs/wire-api/test/golden/testObject_CreateScimToken_4.json index cd71c759b31..fe498e30619 100644 --- a/libs/wire-api/test/golden/testObject_CreateScimToken_4.json +++ b/libs/wire-api/test/golden/testObject_CreateScimToken_4.json @@ -1,6 +1,6 @@ { "description": "description4", + "name": "scim connection name", "password": null, - "verification_code": null, - "name": "scim connection name" + "verification_code": null } diff --git a/libs/wire-api/test/golden/testObject_CreatedApp_1.json b/libs/wire-api/test/golden/testObject_CreatedApp_1.json new file mode 100644 index 00000000000..a9c0237d56c --- /dev/null +++ b/libs/wire-api/test/golden/testObject_CreatedApp_1.json @@ -0,0 +1,33 @@ +{ + "cookie": "DTHdPvHSFolvyGVvuaexZ9DKptwnxTSn8UhKc-6A9q34s4q0YY3_CgpYxDMr56crHrW79EPwKu2BLwQkFT7wBw==.v=1.k=1.d=1773661988.t=u.l=.u=ac638199-8816-439f-88dd-8e206c9b5baa.r=fa16d9df", + "user": { + "accent_id": -1, + "app": { + "category": "other", + "description": "bloob" + }, + "assets": [], + "deleted": true, + "email": "some@example", + "expires_at": "1864-05-09T01:42:22.437Z", + "handle": "emsonpvo3-x_4ys4qjtjtkfgx.mag6pi2ldq.77m5vnsn_tte41r-0vwgklpeejr1t4se0bknu4tsuqs-njzh34-ba_mj8lm5x6aro4o.2wsqe0ldx", + "id": "00000002-0000-0002-0000-000000000001", + "legalhold_status": "no_consent", + "name": "si4v󴃿\u001b^'ゟk喁\u0015?􈒳\u0000Bw;\u00083*R/𨄵lrI", + "picture": [], + "qualified_id": { + "domain": "go.7.w-3r8iy2.a", + "id": "00000002-0000-0002-0000-000000000001" + }, + "searchable": true, + "service": { + "id": "00000001-0000-0000-0000-000000000001", + "provider": "00000001-0000-0001-0000-000100000001" + }, + "supported_protocols": [ + "proteus" + ], + "team": "00000000-0000-0002-0000-000200000002", + "type": "app" + } +} diff --git a/libs/wire-api/test/golden/testObject_ListUsersById_user_2.json b/libs/wire-api/test/golden/testObject_ListUsersById_user_2.json index 758f6bafe3b..77b0f024dc8 100644 --- a/libs/wire-api/test/golden/testObject_ListUsersById_user_2.json +++ b/libs/wire-api/test/golden/testObject_ListUsersById_user_2.json @@ -11,14 +11,18 @@ "domain": "example.com", "id": "4f201a43-935e-4e19-8fe0-0a878d3d6e74" }, + "searchable": true, "supported_protocols": [ "proteus" ], - "type": "regular", - "searchable": true + "type": "regular" }, { "accent_id": 0, + "app": { + "category": "other", + "description": "bloob" + }, "assets": [], "id": "eb48b095-d96f-4a94-b4ec-2a1d61447e13", "legalhold_status": "disabled", @@ -28,13 +32,13 @@ "domain": "test.net", "id": "eb48b095-d96f-4a94-b4ec-2a1d61447e13" }, + "searchable": true, "supported_protocols": [ "proteus", "mls" ], "text_status": "text status", - "type": "regular", - "searchable": true + "type": "regular" } ] } diff --git a/libs/wire-api/test/golden/testObject_ListUsersById_user_3.json b/libs/wire-api/test/golden/testObject_ListUsersById_user_3.json index f533f2cde92..134ec75cf61 100644 --- a/libs/wire-api/test/golden/testObject_ListUsersById_user_3.json +++ b/libs/wire-api/test/golden/testObject_ListUsersById_user_3.json @@ -17,11 +17,11 @@ "domain": "example.com", "id": "4f201a43-935e-4e19-8fe0-0a878d3d6e74" }, + "searchable": true, "supported_protocols": [ "proteus" ], - "type": "regular", - "searchable": true + "type": "regular" } ] } diff --git a/libs/wire-api/test/golden/testObject_NewApp_1.json b/libs/wire-api/test/golden/testObject_NewApp_1.json new file mode 100644 index 00000000000..120a8ea460d --- /dev/null +++ b/libs/wire-api/test/golden/testObject_NewApp_1.json @@ -0,0 +1,8 @@ +{ + "accent_id": 0, + "assets": [], + "category": "other", + "description": "good description", + "name": "good name", + "password": "good password" +} diff --git a/libs/wire-api/test/golden/testObject_PutApp_1.json b/libs/wire-api/test/golden/testObject_PutApp_1.json new file mode 100644 index 00000000000..14d8ce60f5f --- /dev/null +++ b/libs/wire-api/test/golden/testObject_PutApp_1.json @@ -0,0 +1,7 @@ +{ + "accent_id": 0, + "assets": [], + "category": "other", + "description": "good description", + "name": "good name" +} diff --git a/libs/wire-api/test/golden/testObject_PutApp_2.json b/libs/wire-api/test/golden/testObject_PutApp_2.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/libs/wire-api/test/golden/testObject_PutApp_2.json @@ -0,0 +1 @@ +{} diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_1.json b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_1.json index 135d06ec74b..4ae92570fbe 100644 --- a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_1.json +++ b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_1.json @@ -12,13 +12,14 @@ "role": "admin", "saml_idp": "", "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": null, "team": "00000000-0000-0000-0000-000000000001", + "type": "app", "user_groups": [ "00000000-0000-0000-0000-000000000001", "00000000-0000-0000-0000-000100000000" - ], - "searchable": true + ] }, { "accent_id": 0, @@ -32,13 +33,14 @@ "role": "partner", "saml_idp": null, "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": { "issuer": "https://example.com/issuer/123", "nameid": "0307979d-c742-4421-954a-9ceb1f22e58f" }, "team": "00000000-0000-0000-0000-000000000001", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } ], "found": -4, diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_11.json b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_11.json index 2821ba53caf..28182c5447a 100644 --- a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_11.json +++ b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_11.json @@ -12,10 +12,11 @@ "role": "partner", "saml_idp": "", "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": null, "team": "00000001-0000-0001-0000-000100000001", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": 0, @@ -29,10 +30,11 @@ "role": "owner", "saml_idp": "", "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": null, "team": "00000001-0000-0000-0000-000100000001", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": null, @@ -46,10 +48,11 @@ "role": "admin", "saml_idp": "", "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": null, "team": "00000000-0000-0000-0000-000000000001", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": null, @@ -63,10 +66,11 @@ "role": null, "saml_idp": "", "scim_external_id": null, + "searchable": true, "sso": null, "team": null, - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": 0, @@ -80,10 +84,11 @@ "role": null, "saml_idp": null, "scim_external_id": null, + "searchable": true, "sso": null, "team": "00000001-0000-0000-0000-000000000000", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": null, @@ -97,10 +102,11 @@ "role": "partner", "saml_idp": "", "scim_external_id": null, + "searchable": true, "sso": null, "team": null, - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": 0, @@ -114,10 +120,11 @@ "role": "owner", "saml_idp": null, "scim_external_id": null, + "searchable": true, "sso": null, "team": "00000000-0000-0001-0000-000000000000", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": 0, @@ -131,13 +138,14 @@ "role": null, "saml_idp": "", "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": { "issuer": "https://example.com/issuer/123", "nameid": "0307979d-c742-4421-954a-9ceb1f22e58f" }, "team": "00000000-0000-0001-0000-000100000001", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } ], "found": -5, diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_12.json b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_12.json index eda548416e9..f801c51d3e8 100644 --- a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_12.json +++ b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_12.json @@ -12,13 +12,14 @@ "role": null, "saml_idp": "", "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": { "issuer": "https://example.com/issuer/123", "nameid": "0307979d-c742-4421-954a-9ceb1f22e58f" }, "team": null, - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } ], "found": 0, diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_13.json b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_13.json index dd273aa4aa9..e2d5ca64164 100644 --- a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_13.json +++ b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_13.json @@ -12,10 +12,11 @@ "role": "member", "saml_idp": "", "scim_external_id": null, + "searchable": true, "sso": null, "team": null, - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": null, @@ -29,10 +30,11 @@ "role": "member", "saml_idp": "", "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": null, "team": "00000000-0000-0001-0000-000000000000", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": 0, @@ -46,10 +48,11 @@ "role": null, "saml_idp": "", "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": null, "team": "00000001-0000-0000-0000-000000000000", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } ], "found": -6, diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_14.json b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_14.json index dba7af884d7..d96114200c7 100644 --- a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_14.json +++ b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_14.json @@ -12,10 +12,11 @@ "role": "admin", "saml_idp": "", "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": null, "team": null, - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } ], "found": 1, diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_15.json b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_15.json index 252925c2a27..86e5fb65db2 100644 --- a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_15.json +++ b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_15.json @@ -12,13 +12,14 @@ "role": "owner", "saml_idp": null, "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": { "issuer": "https://example.com/issuer/123", "nameid": "0307979d-c742-4421-954a-9ceb1f22e58f" }, "team": "00000001-0000-0000-0000-000100000000", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } ], "found": 2, diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_16.json b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_16.json index 7fa3c5fc967..10785345e97 100644 --- a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_16.json +++ b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_16.json @@ -12,13 +12,14 @@ "role": "admin", "saml_idp": "", "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": { "issuer": "https://example.com/issuer/123", "nameid": "0307979d-c742-4421-954a-9ceb1f22e58f" }, "team": null, - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": null, @@ -32,10 +33,11 @@ "role": "admin", "saml_idp": null, "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": null, "team": "00000000-0000-0000-0000-000100000001", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": 0, @@ -49,10 +51,11 @@ "role": "admin", "saml_idp": "", "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": null, "team": "00000000-0000-0001-0000-000000000000", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } ], "found": 2, diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_17.json b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_17.json index 5208e9bb34c..cc9b5b67119 100644 --- a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_17.json +++ b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_17.json @@ -12,10 +12,11 @@ "role": "partner", "saml_idp": "", "scim_external_id": null, + "searchable": true, "sso": null, "team": null, - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } ], "found": -7, diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_18.json b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_18.json index fd0a678a1c0..4d0f55cb95f 100644 --- a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_18.json +++ b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_18.json @@ -12,10 +12,11 @@ "role": "owner", "saml_idp": "", "scim_external_id": null, + "searchable": true, "sso": null, "team": "00000001-0000-0001-0000-000100000001", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } ], "found": 1, diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_3.json b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_3.json index 3b54f14c3ea..2d07d11288d 100644 --- a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_3.json +++ b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_3.json @@ -12,10 +12,11 @@ "role": "admin", "saml_idp": null, "scim_external_id": null, + "searchable": true, "sso": null, "team": "00000001-0000-0001-0000-000000000000", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": 0, @@ -29,10 +30,11 @@ "role": null, "saml_idp": "", "scim_external_id": null, + "searchable": true, "sso": null, "team": null, - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": 0, @@ -46,10 +48,11 @@ "role": null, "saml_idp": "", "scim_external_id": null, + "searchable": true, "sso": null, "team": null, - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } ], "found": -5, diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_4.json b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_4.json index 4669a7197ef..78527f375a4 100644 --- a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_4.json +++ b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_4.json @@ -12,13 +12,14 @@ "role": "owner", "saml_idp": null, "scim_external_id": null, + "searchable": true, "sso": { "issuer": "https://example.com/issuer/123", "nameid": "0307979d-c742-4421-954a-9ceb1f22e58f" }, "team": "00000001-0000-0000-0000-000000000001", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": 0, @@ -32,13 +33,14 @@ "role": "admin", "saml_idp": "", "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": { "issuer": "https://example.com/issuer/123", "nameid": "0307979d-c742-4421-954a-9ceb1f22e58f" }, "team": null, - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": null, @@ -52,13 +54,14 @@ "role": "partner", "saml_idp": "", "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": { "issuer": "https://example.com/issuer/123", "nameid": "0307979d-c742-4421-954a-9ceb1f22e58f" }, "team": "00000001-0000-0000-0000-000100000000", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": 0, @@ -72,13 +75,14 @@ "role": "partner", "saml_idp": "", "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": { "issuer": "https://example.com/issuer/123", "nameid": "0307979d-c742-4421-954a-9ceb1f22e58f" }, "team": null, - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } ], "found": -2, diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_5.json b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_5.json index fb31b748817..26f026d222f 100644 --- a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_5.json +++ b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_5.json @@ -12,10 +12,11 @@ "role": "partner", "saml_idp": "", "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": null, "team": null, - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } ], "found": -2, diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_6.json b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_6.json index f84e70a64ce..1a04558b6a6 100644 --- a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_6.json +++ b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_6.json @@ -12,10 +12,11 @@ "role": "owner", "saml_idp": "", "scim_external_id": null, + "searchable": true, "sso": null, "team": "00000000-0000-0000-0000-000000000000", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": 0, @@ -29,10 +30,11 @@ "role": "owner", "saml_idp": "", "scim_external_id": null, + "searchable": true, "sso": null, "team": null, - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": null, @@ -46,10 +48,11 @@ "role": null, "saml_idp": "", "scim_external_id": null, + "searchable": true, "sso": null, "team": "00000000-0000-0001-0000-000100000000", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": 0, @@ -63,10 +66,11 @@ "role": "owner", "saml_idp": "", "scim_external_id": null, + "searchable": true, "sso": null, "team": "00000000-0000-0000-0000-000000000000", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": 0, @@ -80,13 +84,14 @@ "role": null, "saml_idp": "", "scim_external_id": null, + "searchable": true, "sso": { "issuer": "https://example.com/issuer/123", "nameid": "0307979d-c742-4421-954a-9ceb1f22e58f" }, "team": null, - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": null, @@ -100,10 +105,11 @@ "role": "member", "saml_idp": "", "scim_external_id": null, + "searchable": true, "sso": null, "team": "00000001-0000-0001-0000-000100000001", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": 0, @@ -117,10 +123,11 @@ "role": "admin", "saml_idp": null, "scim_external_id": null, + "searchable": true, "sso": null, "team": null, - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": 0, @@ -134,13 +141,14 @@ "role": null, "saml_idp": "", "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": { "issuer": "https://example.com/issuer/123", "nameid": "0307979d-c742-4421-954a-9ceb1f22e58f" }, "team": "00000001-0000-0001-0000-000100000000", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": null, @@ -154,10 +162,11 @@ "role": "partner", "saml_idp": null, "scim_external_id": null, + "searchable": true, "sso": null, "team": "00000000-0000-0001-0000-000100000001", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": 0, @@ -171,10 +180,11 @@ "role": "owner", "saml_idp": "", "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": null, "team": "00000001-0000-0001-0000-000000000001", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": 0, @@ -188,10 +198,11 @@ "role": "member", "saml_idp": "", "scim_external_id": null, + "searchable": true, "sso": null, "team": "00000001-0000-0000-0000-000000000000", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": 0, @@ -205,13 +216,14 @@ "role": "partner", "saml_idp": "", "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": { "issuer": "https://example.com/issuer/123", "nameid": "0307979d-c742-4421-954a-9ceb1f22e58f" }, "team": "00000001-0000-0001-0000-000100000000", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": 0, @@ -225,10 +237,11 @@ "role": "partner", "saml_idp": null, "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": null, "team": null, - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } ], "found": -4, diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_7.json b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_7.json index 1168b9bf3fc..1747dca0692 100644 --- a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_7.json +++ b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_7.json @@ -12,10 +12,11 @@ "role": null, "saml_idp": "", "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": null, "team": null, - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": 0, @@ -29,10 +30,11 @@ "role": "member", "saml_idp": "", "scim_external_id": null, + "searchable": true, "sso": null, "team": null, - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": 0, @@ -46,10 +48,11 @@ "role": "partner", "saml_idp": "", "scim_external_id": null, + "searchable": true, "sso": null, "team": "00000000-0000-0000-0000-000000000000", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": 0, @@ -63,10 +66,11 @@ "role": null, "saml_idp": "", "scim_external_id": null, + "searchable": true, "sso": null, "team": null, - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": 0, @@ -80,10 +84,11 @@ "role": "member", "saml_idp": "", "scim_external_id": null, + "searchable": true, "sso": null, "team": "00000000-0000-0000-0000-000100000000", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": 0, @@ -97,13 +102,14 @@ "role": "partner", "saml_idp": "", "scim_external_id": null, + "searchable": true, "sso": { "issuer": "https://example.com/issuer/123", "nameid": "0307979d-c742-4421-954a-9ceb1f22e58f" }, "team": "00000001-0000-0001-0000-000100000001", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } ], "found": 1, diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_8.json b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_8.json index ed6bc8d72c5..5074da85e32 100644 --- a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_8.json +++ b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_8.json @@ -12,10 +12,11 @@ "role": "owner", "saml_idp": "", "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": null, "team": "00000001-0000-0000-0000-000100000001", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": 0, @@ -29,10 +30,11 @@ "role": "owner", "saml_idp": null, "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": null, "team": null, - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": 0, @@ -46,10 +48,11 @@ "role": null, "saml_idp": "", "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": null, "team": "00000001-0000-0000-0000-000100000001", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": 0, @@ -63,10 +66,11 @@ "role": null, "saml_idp": "", "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": null, "team": "00000000-0000-0001-0000-000000000001", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } ], "found": 7, diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_9.json b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_9.json index c4f33c9e15e..3dbf3afd0b1 100644 --- a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_9.json +++ b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_9.json @@ -12,10 +12,11 @@ "role": "member", "saml_idp": "", "scim_external_id": null, + "searchable": true, "sso": null, "team": null, - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": 0, @@ -29,13 +30,14 @@ "role": null, "saml_idp": "", "scim_external_id": null, + "searchable": true, "sso": { "issuer": "https://example.com/issuer/123", "nameid": "0307979d-c742-4421-954a-9ceb1f22e58f" }, "team": "00000000-0000-0000-0000-000000000001", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": 0, @@ -49,13 +51,14 @@ "role": "member", "saml_idp": "", "scim_external_id": null, + "searchable": true, "sso": { "issuer": "https://example.com/issuer/123", "nameid": "0307979d-c742-4421-954a-9ceb1f22e58f" }, "team": "00000001-0000-0000-0000-000100000000", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] }, { "accent_id": null, @@ -69,13 +72,14 @@ "role": "partner", "saml_idp": "", "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": { "issuer": "https://example.com/issuer/123", "nameid": "0307979d-c742-4421-954a-9ceb1f22e58f" }, "team": "00000000-0000-0000-0000-000100000001", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } ], "found": 2, diff --git a/libs/wire-api/test/golden/testObject_SelfProfile_user_1.json b/libs/wire-api/test/golden/testObject_SelfProfile_user_1.json index 0da295244f9..968efaafb6c 100644 --- a/libs/wire-api/test/golden/testObject_SelfProfile_user_1.json +++ b/libs/wire-api/test/golden/testObject_SelfProfile_user_1.json @@ -13,6 +13,7 @@ "domain": "n0-994.m-226.f91.vg9p-mj-j2", "id": "00000001-0000-0000-0000-000000000002" }, + "searchable": true, "service": { "id": "00000000-0000-0001-0000-000000000000", "provider": "00000000-0000-0001-0000-000000000001" @@ -23,6 +24,5 @@ ], "team": "00000001-0000-0002-0000-000000000002", "text_status": "text status", - "searchable": true, "type": "regular" } diff --git a/libs/wire-api/test/golden/testObject_TeamContact_user_1.json b/libs/wire-api/test/golden/testObject_TeamContact_user_1.json index ed8bc483bb5..ad36db9b80d 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_1.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_1.json @@ -10,8 +10,9 @@ "role": "admin", "saml_idp": "r", "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": null, "team": null, - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } diff --git a/libs/wire-api/test/golden/testObject_TeamContact_user_10.json b/libs/wire-api/test/golden/testObject_TeamContact_user_10.json index 662028a8fb0..a6d1a789f88 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_10.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_10.json @@ -10,11 +10,12 @@ "role": "member", "saml_idp": "P-\u0019", "scim_external_id": null, + "searchable": true, "sso": { "issuer": "https://example.com/issuer/123", "nameid": "0307979d-c742-4421-954a-9ceb1f22e58f" }, "team": null, - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } diff --git a/libs/wire-api/test/golden/testObject_TeamContact_user_11.json b/libs/wire-api/test/golden/testObject_TeamContact_user_11.json index 34f22b4ae06..dbd0016e8bc 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_11.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_11.json @@ -10,8 +10,9 @@ "role": "partner", "saml_idp": null, "scim_external_id": null, + "searchable": true, "sso": null, "team": null, - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } diff --git a/libs/wire-api/test/golden/testObject_TeamContact_user_12.json b/libs/wire-api/test/golden/testObject_TeamContact_user_12.json index 43224e58b9c..ed82c998f72 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_12.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_12.json @@ -10,8 +10,9 @@ "role": null, "saml_idp": "\u001a:", "scim_external_id": null, + "searchable": true, "sso": null, "team": "00000001-0000-0001-0000-000000000000", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } diff --git a/libs/wire-api/test/golden/testObject_TeamContact_user_13.json b/libs/wire-api/test/golden/testObject_TeamContact_user_13.json index 6037fc852ac..f449df749ee 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_13.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_13.json @@ -10,8 +10,9 @@ "role": "member", "saml_idp": "󲥹\u0007", "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": null, "team": "00000000-0000-0002-0000-000200000000", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } diff --git a/libs/wire-api/test/golden/testObject_TeamContact_user_14.json b/libs/wire-api/test/golden/testObject_TeamContact_user_14.json index cdc85510cb2..f89c03c5f0e 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_14.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_14.json @@ -10,8 +10,9 @@ "role": "partner", "saml_idp": null, "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": null, "team": null, - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } diff --git a/libs/wire-api/test/golden/testObject_TeamContact_user_15.json b/libs/wire-api/test/golden/testObject_TeamContact_user_15.json index 58093cb0b9e..c7b314a6a52 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_15.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_15.json @@ -10,8 +10,9 @@ "role": "partner", "saml_idp": "", "scim_external_id": null, + "searchable": true, "sso": null, "team": null, - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } diff --git a/libs/wire-api/test/golden/testObject_TeamContact_user_16.json b/libs/wire-api/test/golden/testObject_TeamContact_user_16.json index 4da3aec7835..91d545abfaf 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_16.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_16.json @@ -10,11 +10,12 @@ "role": null, "saml_idp": "k", "scim_external_id": null, + "searchable": true, "sso": { "issuer": "https://example.com/issuer/123", "nameid": "0307979d-c742-4421-954a-9ceb1f22e58f" }, "team": "00000001-0000-0002-0000-000200000001", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } diff --git a/libs/wire-api/test/golden/testObject_TeamContact_user_17.json b/libs/wire-api/test/golden/testObject_TeamContact_user_17.json index a3d5ed637ff..2494790fee8 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_17.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_17.json @@ -10,11 +10,12 @@ "role": "owner", "saml_idp": "𡭄", "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": { "issuer": "https://example.com/issuer/123", "nameid": "0307979d-c742-4421-954a-9ceb1f22e58f" }, "team": "00000002-0000-0000-0000-000200000001", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } diff --git a/libs/wire-api/test/golden/testObject_TeamContact_user_18.json b/libs/wire-api/test/golden/testObject_TeamContact_user_18.json index fe849fb1ebc..467de3abd44 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_18.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_18.json @@ -10,11 +10,12 @@ "role": "owner", "saml_idp": "\u0012", "scim_external_id": null, + "searchable": true, "sso": { "issuer": "https://example.com/issuer/123", "nameid": "0307979d-c742-4421-954a-9ceb1f22e58f" }, "team": "00000001-0000-0002-0000-000000000000", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } diff --git a/libs/wire-api/test/golden/testObject_TeamContact_user_19.json b/libs/wire-api/test/golden/testObject_TeamContact_user_19.json index a8ec6126b67..727c5eeee8e 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_19.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_19.json @@ -10,11 +10,12 @@ "role": "partner", "saml_idp": null, "scim_external_id": null, + "searchable": true, "sso": { "issuer": "https://example.com/issuer/123", "nameid": "0307979d-c742-4421-954a-9ceb1f22e58f" }, "team": "00000001-0000-0002-0000-000200000002", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } diff --git a/libs/wire-api/test/golden/testObject_TeamContact_user_2.json b/libs/wire-api/test/golden/testObject_TeamContact_user_2.json index 0eccb2d78c4..d1ed161ae26 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_2.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_2.json @@ -10,8 +10,9 @@ "role": "partner", "saml_idp": "N\u0014", "scim_external_id": null, + "searchable": true, "sso": null, "team": "00000002-0000-0000-0000-000200000001", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } diff --git a/libs/wire-api/test/golden/testObject_TeamContact_user_20.json b/libs/wire-api/test/golden/testObject_TeamContact_user_20.json index 9543dc6159b..2e42d6cd3a4 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_20.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_20.json @@ -10,8 +10,9 @@ "role": "owner", "saml_idp": "", "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": null, "team": "00000001-0000-0002-0000-000100000001", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } diff --git a/libs/wire-api/test/golden/testObject_TeamContact_user_3.json b/libs/wire-api/test/golden/testObject_TeamContact_user_3.json index da97199e583..44bbbc0ae76 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_3.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_3.json @@ -10,11 +10,12 @@ "role": "member", "saml_idp": "\"c`", "scim_external_id": null, + "searchable": true, "sso": { "issuer": "https://example.com/issuer/123", "nameid": "0307979d-c742-4421-954a-9ceb1f22e58f" }, "team": "00000002-0000-0002-0000-000100000000", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } diff --git a/libs/wire-api/test/golden/testObject_TeamContact_user_4.json b/libs/wire-api/test/golden/testObject_TeamContact_user_4.json index 9cd57e82f66..5ed5258e1c6 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_4.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_4.json @@ -10,8 +10,9 @@ "role": null, "saml_idp": null, "scim_external_id": null, + "searchable": true, "sso": null, "team": null, - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } diff --git a/libs/wire-api/test/golden/testObject_TeamContact_user_5.json b/libs/wire-api/test/golden/testObject_TeamContact_user_5.json index 321bfb6205e..7bf39aaf9e1 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_5.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_5.json @@ -10,8 +10,9 @@ "role": "partner", "saml_idp": "ㅡ", "scim_external_id": null, + "searchable": true, "sso": null, "team": "00000002-0000-0000-0000-000200000000", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } diff --git a/libs/wire-api/test/golden/testObject_TeamContact_user_6.json b/libs/wire-api/test/golden/testObject_TeamContact_user_6.json index da64356e182..825a7e84ac9 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_6.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_6.json @@ -10,8 +10,9 @@ "role": null, "saml_idp": null, "scim_external_id": null, + "searchable": true, "sso": null, "team": null, - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } diff --git a/libs/wire-api/test/golden/testObject_TeamContact_user_7.json b/libs/wire-api/test/golden/testObject_TeamContact_user_7.json index e211ad842ab..450c3369120 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_7.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_7.json @@ -10,8 +10,9 @@ "role": "admin", "saml_idp": null, "scim_external_id": "0307979d-c742-4421-954a-9ceb1f22e58f", + "searchable": true, "sso": null, "team": null, - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } diff --git a/libs/wire-api/test/golden/testObject_TeamContact_user_8.json b/libs/wire-api/test/golden/testObject_TeamContact_user_8.json index a322226d276..85edf61f098 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_8.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_8.json @@ -10,8 +10,9 @@ "role": "member", "saml_idp": "", "scim_external_id": null, + "searchable": true, "sso": null, "team": "00000002-0000-0000-0000-000100000002", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } diff --git a/libs/wire-api/test/golden/testObject_TeamContact_user_9.json b/libs/wire-api/test/golden/testObject_TeamContact_user_9.json index f0b60ff91e5..1c6fddc1a77 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_9.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_9.json @@ -10,8 +10,9 @@ "role": "admin", "saml_idp": "𨊾\u001f", "scim_external_id": null, + "searchable": true, "sso": null, "team": "00000001-0000-0000-0000-000200000001", - "user_groups": [], - "searchable": true + "type": "regular", + "user_groups": [] } diff --git a/libs/wire-api/test/golden/testObject_UserEvent_1.json b/libs/wire-api/test/golden/testObject_UserEvent_1.json index f2b5b821e9c..dc5d102815b 100644 --- a/libs/wire-api/test/golden/testObject_UserEvent_1.json +++ b/libs/wire-api/test/golden/testObject_UserEvent_1.json @@ -13,13 +13,13 @@ "domain": "foo.example.com", "id": "539d9183-32a5-4fc4-ba5c-4634454e7585" }, + "searchable": true, "status": "deleted", "supported_protocols": [ "proteus" ], "team": "bb843450-b2f5-4ec8-90bd-52c7d5f1d22e", "text_status": "text status", - "searchable": true, "type": "regular" } } diff --git a/libs/wire-api/test/golden/testObject_UserEvent_2.json b/libs/wire-api/test/golden/testObject_UserEvent_2.json index 74180f5dea8..b20cd31b318 100644 --- a/libs/wire-api/test/golden/testObject_UserEvent_2.json +++ b/libs/wire-api/test/golden/testObject_UserEvent_2.json @@ -13,13 +13,13 @@ "domain": "foo.example.com", "id": "539d9183-32a5-4fc4-ba5c-4634454e7585" }, + "searchable": true, "status": "deleted", "supported_protocols": [ "proteus" ], "team": "bb843450-b2f5-4ec8-90bd-52c7d5f1d22e", "text_status": "text status", - "searchable": true, "type": "regular" } } diff --git a/libs/wire-api/test/golden/testObject_UserProfile_user_1.json b/libs/wire-api/test/golden/testObject_UserProfile_user_1.json index 3f3765ebd44..9885b7d9dae 100644 --- a/libs/wire-api/test/golden/testObject_UserProfile_user_1.json +++ b/libs/wire-api/test/golden/testObject_UserProfile_user_1.json @@ -9,10 +9,10 @@ "domain": "v.ay64d", "id": "00000002-0000-0001-0000-000000000000" }, + "searchable": true, "supported_protocols": [ "proteus" ], "text_status": "text status", - "type": "regular", - "searchable": true + "type": "regular" } diff --git a/libs/wire-api/test/golden/testObject_UserProfile_user_2.json b/libs/wire-api/test/golden/testObject_UserProfile_user_2.json index cc4db1c412f..1e4308b7855 100644 --- a/libs/wire-api/test/golden/testObject_UserProfile_user_2.json +++ b/libs/wire-api/test/golden/testObject_UserProfile_user_2.json @@ -1,5 +1,9 @@ { "accent_id": -1, + "app": { + "category": "other", + "description": "bloob" + }, "assets": [], "deleted": true, "email": "some@example", @@ -13,6 +17,7 @@ "domain": "go.7.w-3r8iy2.a", "id": "00000002-0000-0002-0000-000000000001" }, + "searchable": true, "service": { "id": "00000001-0000-0000-0000-000000000001", "provider": "00000001-0000-0001-0000-000100000001" @@ -21,6 +26,5 @@ "proteus" ], "team": "00000000-0000-0002-0000-000200000002", - "type": "app", - "searchable": true + "type": "app" } diff --git a/libs/wire-api/test/golden/testObject_User_user_1.json b/libs/wire-api/test/golden/testObject_User_user_1.json index 0b153c17495..7dae81c5ed1 100644 --- a/libs/wire-api/test/golden/testObject_User_user_1.json +++ b/libs/wire-api/test/golden/testObject_User_user_1.json @@ -11,10 +11,10 @@ "domain": "s-f4.s", "id": "00000002-0000-0001-0000-000200000002" }, + "searchable": true, "status": "deleted", "supported_protocols": [ "proteus" ], - "searchable": true, "type": "regular" } diff --git a/libs/wire-api/test/golden/testObject_User_user_2.json b/libs/wire-api/test/golden/testObject_User_user_2.json index ea77c4a8add..84b0f0b96d4 100644 --- a/libs/wire-api/test/golden/testObject_User_user_2.json +++ b/libs/wire-api/test/golden/testObject_User_user_2.json @@ -28,6 +28,7 @@ "domain": "k.vbg.p", "id": "00000000-0000-0001-0000-000200000001" }, + "searchable": true, "service": { "id": "00000000-0000-0000-0000-000000000001", "provider": "00000000-0000-0000-0000-000100000000" @@ -35,6 +36,5 @@ "status": "deleted", "supported_protocols": [], "text_status": "text status", - "searchable": true, "type": "bot" } diff --git a/libs/wire-api/test/golden/testObject_User_user_3.json b/libs/wire-api/test/golden/testObject_User_user_3.json index f5ad7cce79c..39e9c67fc79 100644 --- a/libs/wire-api/test/golden/testObject_User_user_3.json +++ b/libs/wire-api/test/golden/testObject_User_user_3.json @@ -14,6 +14,7 @@ "domain": "dt.n", "id": "00000002-0000-0000-0000-000100000002" }, + "searchable": true, "service": { "id": "00000001-0000-0001-0000-000100000000", "provider": "00000001-0000-0000-0000-000100000000" @@ -23,6 +24,5 @@ "proteus" ], "team": "00000002-0000-0001-0000-000200000000", - "searchable": true, "type": "regular" } diff --git a/libs/wire-api/test/golden/testObject_User_user_4.json b/libs/wire-api/test/golden/testObject_User_user_4.json index ef579f758db..495e380f37d 100644 --- a/libs/wire-api/test/golden/testObject_User_user_4.json +++ b/libs/wire-api/test/golden/testObject_User_user_4.json @@ -13,6 +13,7 @@ "domain": "28b.cqb", "id": "00000000-0000-0002-0000-000200000002" }, + "searchable": true, "service": { "id": "00000000-0000-0001-0000-000100000000", "provider": "00000000-0000-0000-0000-000000000000" @@ -25,6 +26,5 @@ "proteus" ], "team": "00000000-0000-0000-0000-000100000002", - "searchable": true, "type": "regular" } diff --git a/libs/wire-api/test/golden/testObject_User_user_5.json b/libs/wire-api/test/golden/testObject_User_user_5.json index 8a934426b23..eb4bd598e86 100644 --- a/libs/wire-api/test/golden/testObject_User_user_5.json +++ b/libs/wire-api/test/golden/testObject_User_user_5.json @@ -13,6 +13,7 @@ "domain": "28b.cqb", "id": "00000000-0000-0002-0000-000200000002" }, + "searchable": true, "service": { "id": "00000000-0000-0001-0000-000100000000", "provider": "00000000-0000-0000-0000-000000000000" @@ -22,6 +23,5 @@ "proteus" ], "team": "00000000-0000-0000-0000-000100000002", - "searchable": true, "type": "regular" } 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 771cdfb7845..6533d66b963 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,7 +26,6 @@ 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 @@ -87,7 +86,7 @@ import Wire.API.Wrapped qualified as Wrapped tests :: T.TestTree tests = T.localOption (T.Timeout (60 * 1000000) "60s") . T.testGroup "JSON roundtrip tests" $ - [ testRoundTrip @App.Category, + [ testRoundTrip @User.Category, testRoundTrip @Asset.AssetToken, testRoundTrip @Asset.NewAssetToken, testRoundTrip @Asset.AssetRetention, diff --git a/libs/wire-api/test/unit/Test/Wire/API/User.hs b/libs/wire-api/test/unit/Test/Wire/API/User.hs index 684a5f9fb85..ce4db2890ea 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/User.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/User.hs @@ -63,7 +63,7 @@ testEmailVisibleToSelf :: TestTree testEmailVisibleToSelf = testProperty "should not contain email when email visibility is EmailVisibleToSelf" $ \user lhStatus -> - let profile = mkUserProfile EmailVisibleToSelf UserTypeRegular user lhStatus + let profile = mkUserProfile EmailVisibleToSelf user Nothing lhStatus in profileEmail profile === Nothing .&&. profileLegalholdStatus profile === lhStatus @@ -71,7 +71,7 @@ testEmailVisibleIfOnTeam :: TestTree testEmailVisibleIfOnTeam = testProperty "should contain email only if the user has one and is part of a team when email visibility is EmailVisibleIfOnTeam" $ \user lhStatus -> - let profile = mkUserProfile EmailVisibleIfOnTeam UserTypeRegular user lhStatus + let profile = mkUserProfile EmailVisibleIfOnTeam user Nothing lhStatus in (profileEmail profile === (userTeam user *> userEmail user)) .&&. profileLegalholdStatus profile === lhStatus @@ -81,13 +81,13 @@ testEmailVisibleIfOnSameTeam = where testNoViewerTeam = testProperty "should not contain email when viewer is not part of a team" $ \user lhStatus -> - let profile = mkUserProfile (EmailVisibleIfOnSameTeam Nothing) UserTypeRegular user lhStatus + let profile = mkUserProfile (EmailVisibleIfOnSameTeam Nothing) user Nothing lhStatus in (profileEmail profile === Nothing) .&&. profileLegalholdStatus profile === lhStatus testViewerDifferentTeam = testProperty "should not contain email when viewer is not part of the same team" $ \viewerTeamId viewerMembership user lhStatus -> - let profile = mkUserProfile (EmailVisibleIfOnSameTeam (Just (viewerTeamId, viewerMembership))) UserTypeRegular user lhStatus + let profile = mkUserProfile (EmailVisibleIfOnSameTeam (Just (viewerTeamId, viewerMembership))) user Nothing lhStatus in Just viewerTeamId /= userTeam user ==> ( profileEmail profile === Nothing .&&. profileLegalholdStatus profile === lhStatus @@ -97,7 +97,7 @@ testEmailVisibleIfOnSameTeam = \viewerTeamId (viewerMembershipNoRole :: TeamMember) userNoTeam lhStatus -> let user = userNoTeam {userTeam = Just viewerTeamId} viewerMembership = viewerMembershipNoRole & TeamMember.permissions .~ TeamMember.rolePermissions RoleExternalPartner - profile = mkUserProfile (EmailVisibleIfOnSameTeam (Just (viewerTeamId, viewerMembership))) UserTypeRegular user lhStatus + profile = mkUserProfile (EmailVisibleIfOnSameTeam (Just (viewerTeamId, viewerMembership))) user Nothing lhStatus in ( profileEmail profile === Nothing .&&. profileLegalholdStatus profile === lhStatus ) @@ -106,7 +106,7 @@ testEmailVisibleIfOnSameTeam = \viewerTeamId (viewerMembershipNoRole :: TeamMember) viewerRole userNoTeam lhStatus -> let user = userNoTeam {userTeam = Just viewerTeamId} viewerMembership = viewerMembershipNoRole & TeamMember.permissions .~ TeamMember.rolePermissions viewerRole - profile = mkUserProfile (EmailVisibleIfOnSameTeam (Just (viewerTeamId, viewerMembership))) UserTypeRegular user lhStatus + profile = mkUserProfile (EmailVisibleIfOnSameTeam (Just (viewerTeamId, viewerMembership))) user Nothing lhStatus in viewerRole /= RoleExternalPartner ==> ( profileEmail profile === userEmail user .&&. profileLegalholdStatus profile === lhStatus @@ -134,6 +134,7 @@ testUserProfile = do profileLegalholdStatus = UserLegalHoldNoConsent, profileSupportedProtocols = defSupportedProtocols, profileType = UserTypeRegular, + profileApp = Nothing, profileSearchable = True } let profileJSONAsText = show $ Aeson.encode userProfile diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index f2b41c8bd34..841fc2114b6 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -69,7 +69,6 @@ library -- cabal-fmt: expand src exposed-modules: Wire.API.Allowlists - Wire.API.App Wire.API.ApplyMods Wire.API.Asset Wire.API.BackgroundJobs @@ -616,6 +615,7 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Generated.Wrapped_20_22some_5fint_22_20Int_user Test.Wire.API.Golden.Manual Test.Wire.API.Golden.Manual.Activate_user + Test.Wire.API.Golden.Manual.App Test.Wire.API.Golden.Manual.CannonId Test.Wire.API.Golden.Manual.ClientCapability Test.Wire.API.Golden.Manual.ClientCapabilityList @@ -675,6 +675,7 @@ test-suite wire-api-golden-tests , bytestring-conversion , containers >=0.5 , currency-codes + , http-api-data , imports , iso3166-country-codes , iso639 diff --git a/libs/wire-subsystems/src/Wire/AppStore.hs b/libs/wire-subsystems/src/Wire/AppStore.hs index f51558e5575..7a166429970 100644 --- a/libs/wire-subsystems/src/Wire/AppStore.hs +++ b/libs/wire-subsystems/src/Wire/AppStore.hs @@ -26,8 +26,8 @@ import Data.Range import Data.UUID import Imports import Polysemy -import Wire.API.App as App import Wire.API.PostgresMarshall +import Wire.API.User import Wire.Arbitrary data StoredApp = StoredApp @@ -58,7 +58,7 @@ instance PostgresMarshall (UUID, UUID, Value, Text, Text, UUID) StoredApp where ( postgresMarshall app.id, postgresMarshall app.teamId, postgresMarshall app.meta, - postgresMarshall (categoryToText app.category), + postgresMarshall (fromCategory app.category), postgresMarshall (fromRange app.description), postgresMarshall app.creator ) @@ -69,7 +69,7 @@ instance PostgresUnmarshall (UUID, UUID, Value, Text, Text, UUID) StoredApp wher <$> postgresUnmarshall uid <*> postgresUnmarshall teamId <*> postgresUnmarshall meta - <*> (postgresUnmarshall =<< maybe (Left $ "Category " <> category <> " not found") Right (categoryFromText category)) + <*> postgresUnmarshall (Category category) <*> (maybe (Left "description out of bounds") Right . checked @0 @300 =<< postgresUnmarshall description) <*> postgresUnmarshall creator diff --git a/libs/wire-subsystems/src/Wire/AppStore/Postgres.hs b/libs/wire-subsystems/src/Wire/AppStore/Postgres.hs index e71f802b25c..2f46d3ee28d 100644 --- a/libs/wire-subsystems/src/Wire/AppStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/AppStore/Postgres.hs @@ -30,8 +30,8 @@ import Imports import Polysemy import Polysemy.Error (Error) import Polysemy.Input -import Wire.API.App qualified as App import Wire.API.PostgresMarshall +import Wire.API.User qualified as User import Wire.AppStore import Wire.Postgres @@ -72,11 +72,18 @@ getAppImpl :: TeamId -> Sem r (Maybe StoredApp) getAppImpl uid tid = - runStatement (uid, tid) $ - dimapPG - [maybeStatement| select (user_id :: uuid), (team_id :: uuid), (metadata :: json), (category :: text), (description :: text), (creator :: uuid) + eraseMetadata <$$> do + runStatement (uid, tid) $ + dimapPG + [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) |] +-- `metadata` is unused, can be removed from postgres schema. for now +-- we just ignore it instead of removing it from the database to avoid +-- migration issues. ~~fisx +eraseMetadata :: StoredApp -> StoredApp +eraseMetadata sap = sap {meta = mempty} + getAppsImpl :: ( Member (Input Pool) r, Member (Embed IO) r, @@ -85,9 +92,10 @@ getAppsImpl :: TeamId -> Sem r [StoredApp] getAppsImpl tid = - runStatement tid $ - dimapPG - [vectorStatement| select (user_id :: uuid), (team_id :: uuid), (metadata :: json), (category :: text), (description :: text), (creator :: uuid) + eraseMetadata <$$> do + runStatement tid $ + dimapPG + [vectorStatement| select (user_id :: uuid), (team_id :: uuid), (metadata :: json), (category :: text), (description :: text), (creator :: uuid) from apps where team_id = ($1 :: uuid) |] updateAppImpl :: @@ -100,7 +108,7 @@ updateAppImpl :: StoredAppUpdate -> Sem r (Either AppStoreError ()) updateAppImpl (toUUID -> teamId) (toUUID -> appId) upd = do - found <- case (App.categoryToText <$> upd.category, fromRange <$> upd.description) of + found <- case (User.fromCategory <$> upd.category, fromRange <$> upd.description) of (Just cat, Just desc) -> runStatement (cat, desc, appId, teamId) $ [maybeStatement| diff --git a/libs/wire-subsystems/src/Wire/AppSubsystem.hs b/libs/wire-subsystems/src/Wire/AppSubsystem.hs index 4e963b83683..f38059531de 100644 --- a/libs/wire-subsystems/src/Wire/AppSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/AppSubsystem.hs @@ -19,14 +19,15 @@ module Wire.AppSubsystem where +import Data.Default import Data.Id +import Data.Misc import Data.Qualified import Data.RetryAfter import Imports import Network.HTTP.Types.Status import Network.Wai.Utilities.Error qualified as Wai import Polysemy -import Wire.API.App qualified as Apps import Wire.API.User import Wire.API.User.Auth import Wire.Error @@ -35,11 +36,17 @@ data AppSubsystemConfig = AppSubsystemConfig { defaultLocale :: Locale } +instance Default AppSubsystemConfig where + def = AppSubsystemConfig def + data AppSubsystemError = AppSubsystemErrorNoPerm | AppSubsystemErrorNoUser -- The user having created the app not found | AppSubsystemErrorAppUserNotFound -- The user used to "enact" the app not found | AppSubsystemErrorNoApp + deriving (Eq, Show) + +instance Exception AppSubsystemError appSubsystemErrorToHttpError :: AppSubsystemError -> HttpError appSubsystemErrorToHttpError = @@ -50,14 +57,15 @@ appSubsystemErrorToHttpError = AppSubsystemErrorNoApp -> Wai.mkError status404 "app-not-found" "App not found" data AppSubsystem m a where - CreateApp :: Local UserId -> TeamId -> Apps.NewApp -> AppSubsystem m Apps.CreatedApp - GetApp :: Local UserId -> TeamId -> UserId -> AppSubsystem m Apps.GetApp - GetApps :: Local UserId -> TeamId -> AppSubsystem m Apps.GetAppList - UpdateApp :: Local UserId -> TeamId -> UserId -> Apps.PutApp -> AppSubsystem m () + CreateApp :: Local UserId -> TeamId -> NewApp -> AppSubsystem m CreatedApp + GetApp :: Local UserId -> TeamId -> UserId -> AppSubsystem m AppInfo + GetApps :: Local UserId -> TeamId -> AppSubsystem m [(UserId, AppInfo)] + UpdateApp :: Local UserId -> TeamId -> UserId -> PutApp -> AppSubsystem m () RefreshAppCookie :: Local UserId -> TeamId -> UserId -> + Maybe PlainTextPassword6 -> AppSubsystem m (Either RetryAfter SomeUserToken) DeleteApp :: TeamId -> @@ -65,3 +73,6 @@ data AppSubsystem m a where AppSubsystem m () makeSem ''AppSubsystem + +getAppIds :: (Member AppSubsystem r) => Local UserId -> TeamId -> Sem r [UserId] +getAppIds self tid = fst <$$> getApps self tid diff --git a/libs/wire-subsystems/src/Wire/AppSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/AppSubsystem/Interpreter.hs index 16368489d3e..819c692b4b0 100644 --- a/libs/wire-subsystems/src/Wire/AppSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/AppSubsystem/Interpreter.hs @@ -21,12 +21,12 @@ import Data.ByteString.Conversion import Data.Default import Data.Id import Data.Json.Util -import Data.Map qualified as Map +import Data.LegalHold (UserLegalHoldStatus (..)) +import Data.Misc import Data.Qualified import Data.RetryAfter import Data.Set qualified as Set -import Data.UUID.V4 -import Data.ZAuth.Token +import Data.ZAuth.Token (Token (..), Type (U)) import Imports import Polysemy import Polysemy.Error @@ -34,7 +34,6 @@ import Polysemy.Input import Polysemy.TinyLog (TinyLog) import Polysemy.TinyLog qualified as Log import System.Logger.Message qualified as Log -import Wire.API.App qualified as Apps import Wire.API.Event.Team import Wire.API.Team.Member qualified as T import Wire.API.Team.Role qualified as R @@ -44,10 +43,12 @@ import Wire.AppStore (AppStore, StoredApp (..)) import Wire.AppStore qualified as Store import Wire.AppSubsystem import Wire.AuthenticationSubsystem +import Wire.AuthenticationSubsystem.Cookie (revokeAllCookies) import Wire.AuthenticationSubsystem.ZAuth import Wire.GalleyAPIAccess import Wire.NotificationSubsystem import Wire.Sem.Now +import Wire.Sem.Random import Wire.StoredUser import Wire.TeamSubsystem import Wire.TeamSubsystem.Util @@ -58,7 +59,6 @@ import Wire.UserSubsystem (UserSubsystem, internalUpdateSearchIndex) runAppSubsystem :: ( Member UserStore r, Member TinyLog r, - Member (Embed IO) r, Member (Error AppSubsystemError) r, Member (Input AppSubsystemConfig) r, Member GalleyAPIAccess r, @@ -66,50 +66,53 @@ runAppSubsystem :: Member Now r, Member TeamSubsystem r, Member NotificationSubsystem r, - Member AuthenticationSubsystem r, - Member UserSubsystem r + Member Random r ) => + InterpreterFor UserSubsystem (AuthenticationSubsystem ': r) -> + InterpreterFor AuthenticationSubsystem 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 - GetApps lusr tid -> getAppsImpl lusr tid - UpdateApp lusr tid uid put -> updateAppImpl lusr tid uid put - RefreshAppCookie lusr tid appId -> runError $ refreshAppCookieImpl lusr tid appId - DeleteApp tid appId -> deleteAppImpl tid appId +runAppSubsystem runUser runAuth = + interpret $ + runAuth . runUser . \case + CreateApp lusr tid new -> createAppImpl lusr tid new + GetApp lusr tid uid -> getAppImpl lusr tid uid + GetApps lusr tid -> getAppsImpl lusr tid + UpdateApp lusr tid uid put -> updateAppImpl lusr tid uid put + RefreshAppCookie lusr tid appId password -> runError $ refreshAppCookieImpl lusr tid appId password + DeleteApp tid appId -> deleteAppImpl tid appId createAppImpl :: ( Member UserStore r, + Member AppStore r, Member TinyLog r, - Member (Embed IO) r, Member (Error AppSubsystemError) r, Member (Input AppSubsystemConfig) r, Member GalleyAPIAccess r, - Member AppStore r, Member Now r, Member TeamSubsystem r, Member NotificationSubsystem r, Member AuthenticationSubsystem r, - Member UserSubsystem r + Member UserSubsystem r, + Member Random r ) => Local UserId -> TeamId -> - Apps.NewApp -> - Sem r Apps.CreatedApp -createAppImpl lusr tid (Apps.NewApp new password6) = do - verifyUserPasswordError lusr password6 + NewApp -> + Sem r CreatedApp +createAppImpl lusr tid newApp = do + verifyUserPasswordError lusr newApp.password (creator, mem) <- ensureTeamMember lusr tid note AppSubsystemErrorNoPerm $ guard (T.hasPermission mem T.CreateApp) - u <- appNewStoredUser creator new + u <- appNewStoredUser creator newApp let app = StoredApp { id = u.id, teamId = tid, - meta = new.meta, - category = new.category, - description = new.description, + meta = mempty, -- unused, can be removed from postgres schema at some point. + category = newApp.category, + description = newApp.description, creator = tUnqualified lusr } @@ -130,8 +133,12 @@ createAppImpl lusr tid (Apps.NewApp new password6) = do c :: Cookie (Token U) <- newCookie u.id Nothing PersistentCookie Nothing RevokeSameLabel pure - Apps.CreatedApp - { user = newStoredUserToUser (tUntagged (qualifyAs lusr u)), + CreatedApp + { user = + let usr :: User = newStoredUserToUser (tUntagged (qualifyAs lusr u)) + mbApp :: Maybe AppInfo = Just $ storedAppToAppInfo app + lh = UserLegalHoldDisabled -- FUTUREWORK: this needs to be changed as soon as apps can be put under LH. + in mkUserProfile EmailVisibleIfOnTeam usr mbApp lh, cookie = mkSomeToken c.cookieValue } @@ -158,21 +165,18 @@ getAppImpl :: Local UserId -> TeamId -> UserId -> - Sem r Apps.GetApp + Sem r AppInfo 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 - } + pure $ storedAppToAppInfo storedApp + +storedAppToAppInfo :: StoredApp -> AppInfo +storedAppToAppInfo app = + AppInfo + { category = app.category, + description = app.description + } getAppsImpl :: ( Member AppStore r, @@ -182,30 +186,10 @@ getAppsImpl :: ) => Local UserId -> TeamId -> - Sem r Apps.GetAppList + Sem r [(UserId, AppInfo)] getAppsImpl lusr tid = do void $ ensureTeamMember lusr tid - storedApps <- Store.getApps tid - us <- Store.getUsers ((.id) <$> storedApps) - let mkApp (storedApp, u) = - ( u.id, - 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 - } - ) - pure . Apps.GetAppList $ mkApp <$> matchAndZip storedApps us - where - matchAndZip :: [StoredApp] -> [StoredUser] -> [(StoredApp, StoredUser)] - matchAndZip as us = mapMaybe f as - where - f a = (a,) <$> Map.lookup a.id umap - umap = Map.fromList $ (\u -> (u.id, u)) <$> us + Store.getApps tid <&> map \storedApp -> (storedApp.id, storedAppToAppInfo storedApp) updateAppImpl :: ( Member AppStore r, @@ -216,7 +200,7 @@ updateAppImpl :: Local UserId -> TeamId -> UserId -> - Apps.PutApp -> + PutApp -> Sem r () updateAppImpl lusr tid appid upd = do (_updater, umem) <- ensureTeamMember lusr tid @@ -239,37 +223,40 @@ refreshAppCookieImpl :: Local UserId -> TeamId -> UserId -> + Maybe PlainTextPassword6 -> Sem r SomeUserToken -refreshAppCookieImpl (tUnqualified -> uid) tid appId = do +refreshAppCookieImpl (tUnqualified -> uid) tid appId mbPassword = do + reauthenticateEither uid mbPassword + >>= either (const $ throw AppSubsystemErrorNoPerm) (const $ pure ()) + mem <- getTeamMember uid tid >>= note AppSubsystemErrorNoPerm note AppSubsystemErrorNoPerm $ guard (T.hasPermission mem T.ManageApps) void $ Store.getApp appId tid >>= note AppSubsystemErrorNoApp + revokeAllCookies appId c :: Cookie (Token U) <- newCookieLimited appId Nothing PersistentCookie Nothing RevokeSameLabel >>= either throw pure pure $ mkSomeToken c.cookieValue appNewStoredUser :: - ( Member (Embed IO) r, - Member (Input AppSubsystemConfig) r - ) => + (Member (Input AppSubsystemConfig) r, Member Random r) => StoredUser -> - Apps.GetApp -> + NewApp -> Sem r NewStoredUser appNewStoredUser creator new = do - uid <- liftIO nextRandom + uid <- newId defLoc <- inputs defaultLocale let loc = toLocale defLoc (creator.language, creator.country) pure NewStoredUser - { id = Id uid, + { id = uid, userType = UserTypeApp, email = Nothing, ssoId = Nothing, name = new.name, textStatus = Nothing, - pict = new.pict, + pict = Pict [], assets = new.assets, accentId = new.accentId, password = Nothing, diff --git a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem.hs b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem.hs index 53b62aeef8b..acf61f9072b 100644 --- a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem.hs @@ -1,24 +1,8 @@ --- 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 TemplateHaskell #-} -- This file is part of the Wire Server implementation. -- --- Copyright (C) 2025 Wire Swiss GmbH +-- Copyright (C) 2026 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 diff --git a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Cookie.hs b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Cookie.hs index 4148e9e47d6..01543d47073 100644 --- a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Cookie.hs +++ b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Cookie.hs @@ -110,6 +110,12 @@ newCookieLimitedImpl u c typ label policy = do Nothing -> revokeCookiesImpl u evict [] newCookieImpl u c typ label policy +revokeAllCookies :: + (Member AuthenticationSubsystem r) => + UserId -> + Sem r () +revokeAllCookies u = revokeCookies u [] [] + revokeCookiesImpl :: (Member SessionStore r) => UserId -> diff --git a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs index a7fae6f0b45..223a0444942 100644 --- a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs @@ -47,7 +47,7 @@ import Wire.EmailSubsystem import Wire.Events import Wire.HashPassword import Wire.PasswordResetCodeStore -import Wire.PasswordStore (PasswordStore, upsertHashedPassword) +import Wire.PasswordStore (PasswordStore) import Wire.PasswordStore qualified as PasswordStore import Wire.RateLimit import Wire.Sem.Now @@ -55,7 +55,8 @@ import Wire.Sem.Now qualified as Now import Wire.Sem.Random (Random) import Wire.SessionStore import Wire.UserKeyStore -import Wire.UserStore +import Wire.UserStore (UserStore) +import Wire.UserStore qualified as UserStore import Wire.UserSubsystem (UserSubsystem, getLocalAccountBy) import Wire.UserSubsystem qualified as User @@ -119,7 +120,6 @@ instance Exception PasswordResetError where authenticateEitherImpl :: ( Member UserStore r, Member HashPassword r, - Member PasswordStore r, Member RateLimit r ) => UserId -> @@ -127,7 +127,7 @@ authenticateEitherImpl :: Sem r (Either AuthError ()) authenticateEitherImpl uid plaintext = do runError $ - getUserAuthenticationInfo uid >>= \case + UserStore.getUserAuthenticationInfo uid >>= \case Nothing -> throw AuthInvalidUser Just (_, Deleted) -> throw AuthInvalidUser Just (_, Suspended) -> throw AuthSuspended @@ -144,7 +144,7 @@ authenticateEitherImpl uid plaintext = do hashAndUpdatePwd pwd = do tryHashPassword6 rateLimitKey pwd >>= \case Left _ -> pure () - Right hashed -> upsertHashedPassword uid hashed + Right hashed -> UserStore.upsertHashedPassword uid hashed -- | Password reauthentication. If the account has a password, reauthentication -- is mandatory. If @@ -161,7 +161,7 @@ reauthenticateEitherImpl :: Maybe (PlainTextPassword' t) -> Sem r (Either ReAuthError ()) reauthenticateEitherImpl user plaintextMaybe = - getUserAuthenticationInfo user + UserStore.getUserAuthenticationInfo user >>= runError . \case Nothing -> throw (ReAuthError AuthInvalidUser) @@ -296,8 +296,8 @@ resetPasswordImpl :: Member UserSubsystem r, Member HashPassword r, Member SessionStore r, - Member PasswordStore r, - Member RateLimit r + Member RateLimit r, + Member UserStore r ) => PasswordResetIdentity -> PasswordResetCode -> @@ -314,7 +314,7 @@ resetPasswordImpl ident code pw = do Log.debug $ field "user" (toByteString uid) . field "action" (val "User.completePasswordReset") checkNewIsDifferent uid pw hashedPw <- hashPassword8 rateLimitKey pw - PasswordStore.upsertHashedPassword uid hashedPw + UserStore.upsertHashedPassword uid hashedPw codeDelete key deleteAllCookies uid where @@ -330,7 +330,7 @@ resetPasswordImpl ident code pw = do checkNewIsDifferent :: UserId -> PlainTextPassword' t -> Sem r () checkNewIsDifferent uid newPassword = do - mCurrentPassword <- PasswordStore.lookupHashedPassword uid + mCurrentPassword <- UserStore.lookupHashedPassword uid case mCurrentPassword of Just currentPassword -> whenM (verifyPassword (RateLimitUser uid) newPassword currentPassword) $ @@ -369,25 +369,25 @@ verifyProviderPasswordImpl pid plaintext = do verifyPasswordWithStatus (RateLimitProvider pid) plaintext password verifyUserPasswordImpl :: - ( Member PasswordStore r, - Member (Error AuthenticationSubsystemError) r, + ( Member (Error AuthenticationSubsystemError) r, Member HashPassword r, - Member RateLimit r + Member RateLimit r, + Member UserStore r ) => UserId -> PlainTextPassword6 -> Sem r (Bool, PasswordStatus) verifyUserPasswordImpl uid plaintext = do password <- - PasswordStore.lookupHashedPassword uid + UserStore.lookupHashedPassword uid >>= maybe (throw AuthenticationSubsystemBadCredentials) pure verifyPasswordWithStatus (RateLimitUser uid) plaintext password verifyUserPasswordErrorImpl :: - ( Member PasswordStore r, - Member (Error AuthenticationSubsystemError) r, + ( Member (Error AuthenticationSubsystemError) r, Member HashPassword r, - Member RateLimit r + Member RateLimit r, + Member UserStore r ) => Local UserId -> PlainTextPassword6 -> diff --git a/libs/wire-subsystems/src/Wire/ConversationStore.hs b/libs/wire-subsystems/src/Wire/ConversationStore.hs index 49e552c8104..466dec4b230 100644 --- a/libs/wire-subsystems/src/Wire/ConversationStore.hs +++ b/libs/wire-subsystems/src/Wire/ConversationStore.hs @@ -19,9 +19,6 @@ module Wire.ConversationStore where -import Control.Error (lastMay) -import Data.Aeson qualified as Aeson -import Data.ByteString qualified as BS import Data.Id import Data.Misc import Data.Qualified @@ -42,7 +39,6 @@ import Wire.API.MLS.LeafNode import Wire.API.MLS.SubConversation import Wire.API.Pagination import Wire.API.Provider.Service -import Wire.API.Routes.MultiTablePaging import Wire.ConversationStore.MLS.Types import Wire.Sem.Paging.Cassandra import Wire.StoredConversation @@ -154,56 +150,6 @@ acceptConnectConversation cid = setConversationType cid One2OneConv upsertMember :: (Member ConversationStore r) => Local ConvId -> Local UserId -> Sem r [LocalMember] upsertMember c u = fst <$> upsertMembers (tUnqualified c) (UserList [(tUnqualified u, roleNameWireAdmin)] []) -getConversationIdsResultSet :: forall r. (Member ConversationStore r) => Local UserId -> Range 1 1000 Int32 -> Maybe (Qualified ConvId) -> Sem r (ResultSet (Qualified ConvId)) -getConversationIdsResultSet lusr maxIds mLastId = do - case fmap (flip relativeTo lusr) mLastId of - Nothing -> getLocals Nothing - Just (Local (tUnqualified -> lastId)) -> getLocals (Just lastId) - Just (Remote lastId) -> getRemotes (Just lastId) maxIds - where - localDomain = tDomain lusr - usr = tUnqualified lusr - - getLocals :: Maybe ConvId -> Sem r (ResultSet (Qualified ConvId)) - getLocals lastId = do - localPage <- flip Qualified localDomain <$$> getLocalConversationIds usr lastId maxIds - let remainingSize = fromRange maxIds - fromIntegral (length localPage.resultSetResult) - case checked remainingSize of - Nothing -> pure localPage {resultSetType = ResultSetTruncated} - Just checkedRemaining -> do - remotePage <- getRemotes Nothing checkedRemaining - pure - remotePage - { resultSetResult = localPage.resultSetResult <> remotePage.resultSetResult - } - - getRemotes :: Maybe (Remote ConvId) -> Range 1 1000 Int32 -> Sem r (ResultSet (Qualified ConvId)) - getRemotes lastRemote maxRemotes = tUntagged <$$> getRemoteConversationIds usr lastRemote maxRemotes - --- | This function only exists because we use the 'MultiTablePage' type for the --- endpoint. Since now the pagination is based on the qualified ids, we can --- remove the use of this type in future API versions. -getConversationIds :: forall r. (Member ConversationStore r) => Local UserId -> Range 1 1000 Int32 -> Maybe ConversationPagingState -> Sem r ConvIdsPage -getConversationIds lusr maxIds pagingState = do - let mLastId = Aeson.decode . BS.fromStrict =<< (.mtpsState) =<< pagingState - resultSet <- getConversationIdsResultSet lusr maxIds mLastId - let mLastResult = lastMay resultSet.resultSetResult - pure - MultiTablePage - { mtpResults = resultSet.resultSetResult, - mtpHasMore = case resultSet.resultSetType of - ResultSetTruncated -> True - ResultSetComplete -> False, - mtpPagingState = - MultiTablePagingState - { mtpsTable = case fmap (flip relativeTo lusr) mLastResult of - Just (Local _) -> PagingLocals - Just (Remote _) -> PagingRemotes - Nothing -> PagingRemotes, - mtpsState = BS.toStrict . Aeson.encode <$> mLastResult - } - } - getConvOrSubGroupInfo :: (Member ConversationStore r) => ConvOrSubConvId -> diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem.hs index 0fe3d35b1d3..3a4593cf22a 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem.hs @@ -21,11 +21,12 @@ module Wire.ConversationSubsystem where import Data.Id import Data.Qualified +import Data.Range (Range) import Data.Singletons (Sing) import Galley.Types.Clients (Clients) import Imports import Polysemy -import Wire.API.Conversation (ExtraConversationData, NewConv, NewOne2OneConv) +import Wire.API.Conversation (ConvIdsPage, ConversationPagingState, ExtraConversationData, NewConv, NewOne2OneConv) import Wire.API.Conversation.Action import Wire.API.Event.Conversation import Wire.NotificationSubsystem (LocalConversationUpdate) @@ -62,6 +63,14 @@ data ConversationSubsystem m a where Maybe ConnId -> Connect -> ConversationSubsystem m (StoredConversation, Bool) + GetConversations :: + [ConvId] -> + ConversationSubsystem m [StoredConversation] + GetConversationIds :: + Local UserId -> + Range 1 1000 Int32 -> + Maybe ConversationPagingState -> + ConversationSubsystem r ConvIdsPage InternalGetClientIds :: [UserId] -> ConversationSubsystem m Clients InternalGetLocalMember :: ConvId -> diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/CreateInternal.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/CreateInternal.hs new file mode 100644 index 00000000000..f2aab2d82e3 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/CreateInternal.hs @@ -0,0 +1,697 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2026 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.ConversationSubsystem.CreateInternal + ( createGroupConversationGeneric, + createOne2OneConversationLogic, + createProteusSelfConversationLogic, + createConnectConversationLogic, + ) +where + +import Control.Error (headMay) +import Control.Lens hiding ((??)) +import Data.Default +import Data.Id +import Data.Json.Util (ToJSONObject (toJSONObject)) +import Data.Misc (FutureWork (FutureWork)) +import Data.Qualified +import Data.Range +import Data.Set qualified as Set +import Data.UUID.Tagged qualified as U +import GHC.TypeNats +import Galley.Types.Error (InternalError, InvalidInput (..)) +import Imports +import Polysemy +import Polysemy.Error +import Polysemy.Input +import Wire.API.Conversation hiding (Member) +import Wire.API.Conversation qualified as Public +import Wire.API.Conversation.CellsState +import Wire.API.Conversation.Config +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.History (History (HistoryPrivate)) +import Wire.API.Push.V2 qualified as PushV2 +import Wire.API.Team +import Wire.API.Team.Collaborator qualified as CollaboratorPermission +import Wire.API.Team.Feature +import Wire.API.Team.Feature qualified as Conf +import Wire.API.Team.FeatureFlags (notTeamMember) +import Wire.API.Team.LegalHold (LegalholdProtectee (LegalholdPlusFederationNotImplemented)) +import Wire.API.Team.Member +import Wire.API.Team.Permission hiding (self) +import Wire.API.User +import Wire.BackendNotificationQueueAccess (BackendNotificationQueueAccess) +import Wire.BrigAPIAccess +import Wire.ConversationStore (ConversationStore) +import Wire.ConversationStore qualified as ConvStore +import Wire.ConversationSubsystem.One2One +import Wire.ConversationSubsystem.Util +import Wire.FeaturesConfigSubsystem +import Wire.FederationAPIAccess (FederationAPIAccess) +import Wire.LegalHoldStore (LegalHoldStore) +import Wire.NotificationSubsystem as NS +import Wire.Sem.Now (Now) +import Wire.Sem.Now qualified as Now +import Wire.Sem.Random (Random) +import Wire.Sem.Random qualified as Random +import Wire.StoredConversation hiding (convTeam, id_, localOne2OneConvId) +import Wire.StoredConversation as Data (NewConversation (..), convType) +import Wire.StoredConversation qualified as Data +import Wire.TeamCollaboratorsSubsystem +import Wire.TeamStore (TeamStore) +import Wire.TeamStore qualified as TeamStore +import Wire.TeamSubsystem (TeamSubsystem) +import Wire.TeamSubsystem qualified as TeamSubsystem +import Wire.UserList (UserList (UserList), toUserList, ulAddLocal, ulAll, ulFromLocals, ulLocals, ulRemotes) + +createGroupConversationGeneric :: + forall r. + ( Member BrigAPIAccess r, + Member ConversationStore r, + Member (ErrorS 'ConvAccessDenied) r, + Member (Error InvalidInput) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS OperationDenied) r, + Member (ErrorS 'NotConnected) r, + Member (ErrorS 'MLSNotEnabled) r, + Member (ErrorS 'MLSNonEmptyMemberList) r, + Member (ErrorS 'MissingLegalholdConsent) r, + Member (ErrorS 'ChannelsNotEnabled) r, + Member (ErrorS 'NotAnMlsConversation) r, + Member (ErrorS HistoryNotSupported) r, + Member (Input ConversationSubsystemConfig) r, + Member LegalHoldStore r, + Member TeamStore r, + Member FeaturesConfigSubsystem r, + Member TeamCollaboratorsSubsystem r, + Member Random r, + Member TeamSubsystem r, + Member Now r, + Member NotificationSubsystem r, + Member (Error FederationError) r, + Member (Error UnreachableBackends) r, + Member BackendNotificationQueueAccess r, + Member (FederationAPIAccess FederatorClient) r + ) => + Local UserId -> + Maybe ConnId -> + Public.NewConv -> + Sem r StoredConversation +createGroupConversationGeneric lusr conn newConv = do + (nc, fromConvSize -> allUsers) <- newRegularConversation lusr newConv + checkCreateConvPermissions lusr newConv newConv.newConvTeam allUsers + ensureNoLegalholdConflicts allUsers + when (newConv.newConvHistory /= HistoryPrivate && newConv.newConvGroupConvType /= Channel) $ + throwS @HistoryNotSupported + + when (Public.newConvProtocol newConv == BaseProtocolMLSTag) $ do + assertMLSEnabled + + lcnv <- traverse (const Random.newId) lusr + storedConv <- createConversationImpl lcnv lusr nc + notifyConversationCreated lusr conn storedConv def + sendCellsNotification lusr conn storedConv + pure storedConv + +createOne2OneConversationLogic :: + ( Member BrigAPIAccess r, + Member ConversationStore r, + Member (Error FederationError) r, + Member (Error UnreachableBackends) r, + Member (Error InvalidInput) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS OperationDenied) r, + Member (ErrorS 'NonBindingTeam) r, + Member (ErrorS 'NoBindingTeamMembers) r, + Member (ErrorS 'TeamNotFound) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'NotConnected) r, + Member TeamStore r, + Member TeamCollaboratorsSubsystem r, + Member TeamSubsystem r, + Member Now r, + Member NotificationSubsystem r, + Member BackendNotificationQueueAccess r, + Member (FederationAPIAccess FederatorClient) r + ) => + Local UserId -> + ConnId -> + Public.NewOne2OneConv -> + Sem r (StoredConversation, Bool) +createOne2OneConversationLogic lusr zcon j = do + let allUsers = newOne2OneConvMembers lusr j + other <- ensureOne (ulAll lusr allUsers) + when (tUntagged lusr == other) $ + throwS @'InvalidOperation + mtid <- case j.team of + Just ti -> do + foldQualified + lusr + (\lother -> checkBindingTeamPermissions lusr lother (cnvTeamId ti)) + (const (pure Nothing)) + other + Nothing -> ensureConnected lusr allUsers $> Nothing + foldQualified + lusr + (createLegacyOne2OneConversationUnchecked lusr zcon j.name mtid) + (createOne2OneConversationUnchecked lusr zcon j.name mtid . tUntagged) + other + +createProteusSelfConversationLogic :: + (Member ConversationStore r) => + Local UserId -> + Sem r (StoredConversation, Bool) +createProteusSelfConversationLogic lusr = do + let lcnv = fmap Data.selfConv lusr + c <- ConvStore.getConversation (tUnqualified lcnv) + maybe (create lcnv) (\conv -> pure (conv, False)) c + where + create lcnv = do + let nc = + Data.NewConversation + { metadata = (defConversationMetadata (Just (tUnqualified lusr))) {cnvmType = Public.SelfConv}, + users = ulFromLocals [toUserRole (tUnqualified lusr)], + protocol = BaseProtocolProteusTag, + groupId = Nothing + } + conv <- createConversationImpl lcnv lusr nc + pure (conv, True) + +createConversationImpl :: + (Member ConversationStore r) => + Local ConvId -> + Local UserId -> + Data.NewConversation -> + Sem r StoredConversation +createConversationImpl lconv _lusr = + ConvStore.upsertConversation lconv + +createConnectConversationLogic :: + ( Member ConversationStore r, + Member (Error FederationError) r, + Member (Error InternalError) r, + Member (Error InvalidInput) r, + Member (Error UnreachableBackends) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'InvalidOperation) r, + Member NotificationSubsystem r, + Member BackendNotificationQueueAccess r, + Member Now r, + Member (FederationAPIAccess FederatorClient) r + ) => + Local UserId -> + Maybe ConnId -> + Connect -> + Sem r (StoredConversation, Bool) +createConnectConversationLogic lusr conn j = do + lrecipient <- ensureLocal lusr (cRecipient j) + n <- rangeCheckedMaybe (cName j) + let meta = + (defConversationMetadata (Just (tUnqualified lusr))) + { cnvmType = Public.ConnectConv, + cnvmName = fmap fromRange n + } + lcnv <- localOne2OneConvId lusr lrecipient + let nc = + Data.NewConversation + { -- We add only one member, second one gets added later, + -- when the other user accepts the connection request. + users = ulFromLocals [(toUserRole . tUnqualified) lusr], + protocol = BaseProtocolProteusTag, + metadata = meta, + groupId = Nothing + } + mconv <- ConvStore.getConversation (tUnqualified lcnv) + case mconv of + Nothing -> do + conv <- create lcnv nc + pure (conv, True) + Just conv -> do + conv' <- update n conv + pure (conv', False) + where + create lcnv nc = do + conv <- createConversationImpl lcnv lusr nc + notifyConversationCreated lusr conn conv def + notifyConversationUpdated lusr conn j conv + pure conv + update n conv = do + let mems = conv.localMembers + if tUnqualified lusr `isMember` mems + then -- we already were in the conversation, maybe also other + connect n conv + else do + let lcid = qualifyAs lusr conv.id_ + mm <- ConvStore.upsertMember lcid lusr + let conv' = + conv + { localMembers = conv.localMembers <> toList mm + } + if null mems + then -- the conversation was empty + connect n conv' + else do + -- we were not in the conversation, but someone else + conv'' <- acceptOne2One lusr conv' conn + if Data.convType conv'' == Public.ConnectConv + then connect n conv'' + else pure conv'' + connect n conv + | Data.convType conv == Public.ConnectConv = do + n' <- case n of + Just x -> do + ConvStore.setConversationName conv.id_ x + pure . Just $ fromRange x + Nothing -> pure $ Data.convName conv + notifyConversationUpdated lusr conn j conv + pure $ Data.convSetName n' conv + | otherwise = pure conv + +ensureNoLegalholdConflicts :: + ( Member (ErrorS 'MissingLegalholdConsent) r, + Member (Input ConversationSubsystemConfig) r, + Member LegalHoldStore r, + Member TeamStore r, + Member TeamSubsystem r + ) => + UserList UserId -> + Sem r () +ensureNoLegalholdConflicts (UserList locals remotes) = do + let FutureWork _remotes = FutureWork @'LegalholdPlusFederationNotImplemented remotes + whenM (anyLegalholdActivated locals) $ + unlessM (allLegalholdConsentGiven locals) $ + throwS @'MissingLegalholdConsent + +checkCreateConvPermissions :: + ( Member BrigAPIAccess r, + Member (ErrorS 'ConvAccessDenied) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS OperationDenied) r, + Member (ErrorS 'NotConnected) r, + Member (ErrorS 'ChannelsNotEnabled) r, + Member (ErrorS 'NotAnMlsConversation) r, + Member TeamStore r, + Member FeaturesConfigSubsystem r, + Member TeamCollaboratorsSubsystem r, + Member TeamSubsystem r + ) => + Local UserId -> + Public.NewConv -> + Maybe ConvTeamInfo -> + UserList UserId -> + Sem r () +checkCreateConvPermissions lusr newConv Nothing allUsers = do + when (newConv.newConvGroupConvType == Channel) $ throwS @OperationDenied + activated <- listToMaybe <$> lookupActivatedUsers [tUnqualified lusr] + void $ noteS @OperationDenied activated + tm <- getTeamMember (tUnqualified lusr) Nothing + for_ tm $ + permissionCheck AddRemoveConvMember . Just + ensureConnected lusr allUsers +checkCreateConvPermissions lusr newConv (Just tinfo) allUsers = do + let convTeam = cnvTeamId tinfo + mTeamMember <- getTeamMember (tUnqualified lusr) (Just convTeam) + teamAssociation <- case mTeamMember of + Just tm -> pure (Just (Right tm)) + Nothing -> do + Left <$$> internalGetTeamCollaborator convTeam (tUnqualified lusr) + + let checkGroup = do + void $ permissionCheck CreateConversation teamAssociation + when (length allUsers > 1 || Public.newConvProtocol newConv == BaseProtocolMLSTag) $ do + void $ permissionCheck AddRemoveConvMember teamAssociation + case newConv.newConvGroupConvType of + Channel -> do + ensureCreateChannelPermissions tinfo.cnvTeamId mTeamMember + GroupConversation -> checkGroup + MeetingConversation -> checkGroup + + convLocalMemberships <- mapM (flip TeamSubsystem.internalGetTeamMember convTeam) (ulLocals allUsers) + ensureAccessRole (accessRoles newConv) (zip (ulLocals allUsers) convLocalMemberships) + ensureConnectedToLocals (tUnqualified lusr) (notTeamMember (ulLocals allUsers) (catMaybes convLocalMemberships)) + ensureConnectedToRemotes lusr (ulRemotes allUsers) + where + ensureCreateChannelPermissions :: + forall r. + ( Member (ErrorS OperationDenied) r, + Member FeaturesConfigSubsystem r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS 'ChannelsNotEnabled) r, + Member (ErrorS 'NotAnMlsConversation) r + ) => + TeamId -> + Maybe TeamMember -> + Sem r () + ensureCreateChannelPermissions tid (Just tm) = do + channelsConf :: LockableFeature ChannelsConfig <- getFeatureForTeam tid + when (channelsConf.status == FeatureStatusDisabled) $ throwS @'ChannelsNotEnabled + when (Public.newConvProtocol newConv /= BaseProtocolMLSTag) $ throwS @'NotAnMlsConversation + case channelsConf.config.allowedToCreateChannels of + Conf.Everyone -> pure () + Conf.TeamMembers -> void $ permissionCheck AddRemoveConvMember $ Just tm + Conf.Admins -> unless (isAdminOrOwner (tm ^. permissions)) $ throwS @OperationDenied + ensureCreateChannelPermissions _ Nothing = do + throwS @'NotATeamMember + +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 + +createLegacyOne2OneConversationUnchecked :: + ( Member ConversationStore r, + Member (Error FederationError) r, + Member (Error UnreachableBackends) r, + Member (Error InvalidInput) r, + Member Now r, + Member NotificationSubsystem r, + Member BackendNotificationQueueAccess r, + Member (FederationAPIAccess FederatorClient) r + ) => + Local UserId -> + ConnId -> + Maybe (Range 1 256 Text) -> + Maybe TeamId -> + Local UserId -> + Sem r (StoredConversation, Bool) +createLegacyOne2OneConversationUnchecked self zcon name mtid other = do + lcnv <- localOne2OneConvId self other + let meta = + (defConversationMetadata (Just (tUnqualified self))) + { cnvmType = Public.One2OneConv, + cnvmTeam = mtid, + cnvmName = fmap fromRange name + } + let nc = + Data.NewConversation + { users = ulFromLocals (map (toUserRole . tUnqualified) [self, other]), + protocol = BaseProtocolProteusTag, + metadata = meta, + groupId = Nothing + } + mc <- ConvStore.getConversation (tUnqualified lcnv) + case mc of + Just c -> pure (c, False) + Nothing -> do + conv <- createConversationImpl lcnv self nc + notifyConversationCreated self (Just zcon) conv def + pure (conv, True) + +createOne2OneConversationUnchecked :: + ( Member ConversationStore r, + Member (Error FederationError) r, + Member (Error UnreachableBackends) r, + Member Now r, + Member NotificationSubsystem r, + Member BackendNotificationQueueAccess r, + Member (FederationAPIAccess FederatorClient) r + ) => + Local UserId -> + ConnId -> + Maybe (Range 1 256 Text) -> + Maybe TeamId -> + Qualified UserId -> + Sem r (StoredConversation, Bool) +createOne2OneConversationUnchecked self zcon name mtid other = do + let create = + foldQualified + self + createOne2OneConversationLocally + createOne2OneConversationRemotely + create (one2OneConvId BaseProtocolProteusTag (tUntagged self) other) self zcon name mtid other + +createOne2OneConversationLocally :: + ( Member ConversationStore r, + Member (Error FederationError) r, + Member (Error UnreachableBackends) r, + Member Now r, + Member NotificationSubsystem r, + Member BackendNotificationQueueAccess r, + Member (FederationAPIAccess FederatorClient) r + ) => + Local ConvId -> + Local UserId -> + ConnId -> + Maybe (Range 1 256 Text) -> + Maybe TeamId -> + Qualified UserId -> + Sem r (StoredConversation, Bool) +createOne2OneConversationLocally lcnv self zcon name mtid other = do + mc <- ConvStore.getConversation (tUnqualified lcnv) + case mc of + Just c -> pure (c, False) + Nothing -> do + let meta = + (defConversationMetadata (Just (tUnqualified self))) + { cnvmType = Public.One2OneConv, + cnvmTeam = mtid, + cnvmName = fmap fromRange name + } + let nc = + Data.NewConversation + { metadata = meta, + users = fmap toUserRole (toUserList lcnv [tUntagged self, other]), + protocol = BaseProtocolProteusTag, + groupId = Nothing + } + conv <- createConversationImpl lcnv self nc + notifyConversationCreated self (Just zcon) conv def + pure (conv, True) + +createOne2OneConversationRemotely :: + (Member (Error FederationError) r) => + Remote ConvId -> + Local UserId -> + ConnId -> + Maybe (Range 1 256 Text) -> + Maybe TeamId -> + Qualified UserId -> + Sem r (StoredConversation, Bool) +createOne2OneConversationRemotely _ _ _ _name _mtid _ = + throw FederationNotImplemented + +newRegularConversation :: + ( Member (ErrorS 'MLSNonEmptyMemberList) r, + Member (ErrorS OperationDenied) r, + Member (Error InvalidInput) r, + Member (Input ConversationSubsystemConfig) r, + Member ConversationStore r + ) => + Local UserId -> + Public.NewConv -> + Sem r (Data.NewConversation, ConvSizeChecked UserList UserId) +newRegularConversation lusr newConv = do + cfg <- input + let uncheckedUsers = newConvMembers lusr newConv + forM_ newConv.newConvParent $ \parent -> do + mMembership <- ConvStore.getLocalMember parent (tUnqualified lusr) + when (isNothing mMembership) $ + throwS @OperationDenied + users <- case Public.newConvProtocol newConv of + BaseProtocolProteusTag -> checkedConvSize cfg uncheckedUsers + BaseProtocolMLSTag -> do + unless (null uncheckedUsers) $ throwS @'MLSNonEmptyMemberList + pure mempty + let usersWithoutCreator = (,newConvUsersRole newConv) <$> fromConvSize users + newConvUsersRoles = + if newConv.newConvSkipCreator + then usersWithoutCreator + else ulAddLocal (toUserRole (tUnqualified lusr)) usersWithoutCreator + let nc = + Data.NewConversation + { metadata = + Public.ConversationMetadata + { cnvmType = Public.RegularConv, + cnvmCreator = Just (tUnqualified lusr), + cnvmAccess = access newConv, + cnvmAccessRoles = accessRoles newConv, + cnvmName = fmap fromRange newConv.newConvName, + cnvmMessageTimer = newConv.newConvMessageTimer, + cnvmReceiptMode = case Public.newConvProtocol newConv of + BaseProtocolProteusTag -> newConv.newConvReceiptMode + BaseProtocolMLSTag -> Just def, + cnvmTeam = fmap cnvTeamId newConv.newConvTeam, + cnvmGroupConvType = Just newConv.newConvGroupConvType, + cnvmChannelAddPermission = if newConv.newConvGroupConvType == Channel then newConv.newConvChannelAddPermission <|> Just def else Nothing, + cnvmCellsState = + if newConv.newConvCells + then CellsPending + else CellsDisabled, + cnvmParent = newConv.newConvParent, + cnvmHistory = newConv.newConvHistory + }, + users = newConvUsersRoles, + protocol = Public.newConvProtocol newConv, + groupId = Nothing + } + pure (nc, users) + +localOne2OneConvId :: + (Member (Error InvalidInput) r) => + Local UserId -> + Local UserId -> + Sem r (Local ConvId) +localOne2OneConvId self other = do + (x, y) <- toUUIDs (tUnqualified self) (tUnqualified other) + pure . qualifyAs self $ Data.localOne2OneConvId x y + where + toUUIDs :: + (Member (Error InvalidInput) r) => + UserId -> + UserId -> + Sem r (U.UUID U.V4, U.UUID U.V4) + toUUIDs a b = do + a' <- U.fromUUID (toUUID a) & note InvalidUUID4 + b' <- U.fromUUID (toUUID b) & note InvalidUUID4 + pure (a', b') + +accessRoles :: Public.NewConv -> Set AccessRole +accessRoles b = fromMaybe defRole (newConvAccessRoles b) + +access :: Public.NewConv -> [Access] +access a = case Set.toList (Public.newConvAccess a) of + [] -> Data.defRegularConvAccess + (x : xs) -> x : xs + +newConvMembers :: Local x -> Public.NewConv -> UserList UserId +newConvMembers loc body = + UserList (newConvUsers body) [] + <> toUserList loc (newConvQualifiedUsers body) + +newOne2OneConvMembers :: Local x -> Public.NewOne2OneConv -> UserList UserId +newOne2OneConvMembers loc body = + UserList body.users [] + <> toUserList loc body.qualifiedUsers + +ensureOne :: (Member (Error InvalidInput) r) => [a] -> Sem r a +ensureOne [x] = pure x +ensureOne _ = throw (InvalidRange "One-to-one conversations can only have a single invited member") + +assertMLSEnabled :: (Member (Input ConversationSubsystemConfig) r, Member (ErrorS 'MLSNotEnabled) r) => Sem r () +assertMLSEnabled = do + cfg <- input + when (null cfg.mlsKeys) $ throwS @'MLSNotEnabled + +newtype ConvSizeChecked f a = ConvSizeChecked {fromConvSize :: f a} + deriving (Functor, Foldable, Traversable) + deriving newtype (Semigroup, Monoid) + +checkedConvSize :: + (Member (Error InvalidInput) r, Foldable f) => + ConversationSubsystemConfig -> + f a -> + Sem r (ConvSizeChecked f a) +checkedConvSize cfg x = do + let minV :: Integer = 0 + limit = cfg.maxConvSize - 1 + if length x <= fromIntegral limit + then pure (ConvSizeChecked x) + else throwErr (errorMsg minV limit "") + +rangeChecked :: (KnownNat n, KnownNat m, Member (Error InvalidInput) r, Within a n m) => a -> Sem r (Range n m a) +rangeChecked = either throwErr pure . checkedEither +{-# INLINE rangeChecked #-} + +rangeCheckedMaybe :: + (Member (Error InvalidInput) r, KnownNat n, KnownNat m, Within a n m) => + Maybe a -> + Sem r (Maybe (Range n m a)) +rangeCheckedMaybe Nothing = pure Nothing +rangeCheckedMaybe (Just a) = Just <$> rangeChecked a +{-# INLINE rangeCheckedMaybe #-} + +throwErr :: (Member (Error InvalidInput) r) => String -> Sem r a +throwErr = throw . InvalidRange . fromString + +checkBindingTeamPermissions :: + ( Member (ErrorS 'NoBindingTeamMembers) r, + Member (ErrorS 'NonBindingTeam) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS OperationDenied) r, + Member (ErrorS 'TeamNotFound) r, + Member TeamCollaboratorsSubsystem r, + Member TeamStore r, + Member TeamSubsystem r + ) => + Local UserId -> + Local UserId -> + TeamId -> + Sem r (Maybe TeamId) +checkBindingTeamPermissions lusr lother tid = do + mTeamCollaborator <- internalGetTeamCollaborator 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 + TeamStore.getTeamBinding tid >>= \case + Just Binding -> do + when (isJust zusrMembership) $ + verifyMembership tid (tUnqualified lusr) + mOtherTeamCollaborator <- internalGetTeamCollaborator tid (tUnqualified lother) + unless (isJust mOtherTeamCollaborator) $ + verifyMembership tid (tUnqualified lother) + pure (Just tid) + Just _ -> throwS @'NonBindingTeam + Nothing -> throwS @'TeamNotFound + where + guardPerm p m = + if m `hasPermission` p + then pure () + else throwS @OperationDenied + +verifyMembership :: + ( Member (ErrorS 'NoBindingTeamMembers) r, + Member TeamSubsystem r + ) => + TeamId -> + UserId -> + Sem r () +verifyMembership tid u = do + membership <- TeamSubsystem.internalGetTeamMember u tid + when (isNothing membership) $ + throwS @'NoBindingTeamMembers + +sendCellsNotification :: + ( Member NotificationSubsystem r, + Member Now r + ) => + Local UserId -> + Maybe ConnId -> + StoredConversation -> + Sem r () +sendCellsNotification lusr conn conv = do + now <- Now.get + let lconv = qualifyAs lusr conv.id_ + event = CellsEvent (tUntagged lconv) (tUntagged lusr) now CellsConvCreateNoData + when (conv.metadata.cnvmCellsState /= CellsDisabled) $ do + let push = + def + { origin = Just (tUnqualified lusr), + json = toJSONObject event, + isCellsEvent = True, + route = PushV2.RouteAny, + conn + } + NS.pushNotifications [push] diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Fetch.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Fetch.hs new file mode 100644 index 00000000000..f50ba268599 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Fetch.hs @@ -0,0 +1,99 @@ +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2026 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.ConversationSubsystem.Fetch + ( getConversationIdsImpl, + ) +where + +import Control.Error (lastMay) +import Data.Aeson qualified as Aeson +import Data.ByteString qualified as BS +import Data.Id +import Data.Qualified +import Data.Range +import Imports +import Polysemy +import Wire.API.Conversation (ConvIdsPage, ConversationPagingState) +import Wire.API.Routes.MultiTablePaging +import Wire.ConversationStore (ConversationStore) +import Wire.ConversationStore qualified as ConvStore +import Wire.Sem.Paging.Cassandra (ResultSet (..), ResultSetType (..)) + +getConversationIdsResultSet :: + forall r. + (Member ConversationStore r) => + Local UserId -> + Range 1 1000 Int32 -> + Maybe (Qualified ConvId) -> + Sem r (ResultSet (Qualified ConvId)) +getConversationIdsResultSet lusr maxIds mLastId = do + case fmap (flip relativeTo lusr) mLastId of + Nothing -> getLocals Nothing + Just (Local (tUnqualified -> lastId)) -> getLocals (Just lastId) + Just (Remote lastId) -> getRemotes (Just lastId) maxIds + where + localDomain = tDomain lusr + usr = tUnqualified lusr + + getLocals :: Maybe ConvId -> Sem r (ResultSet (Qualified ConvId)) + getLocals lastId = do + localPage <- flip Qualified localDomain <$$> ConvStore.getLocalConversationIds usr lastId maxIds + let remainingSize = fromRange maxIds - fromIntegral (length localPage.resultSetResult) + case checked remainingSize of + Nothing -> pure localPage {resultSetType = ResultSetTruncated} + Just checkedRemaining -> do + remotePage <- getRemotes Nothing checkedRemaining + pure + remotePage + { resultSetResult = localPage.resultSetResult <> remotePage.resultSetResult + } + + getRemotes :: Maybe (Remote ConvId) -> Range 1 1000 Int32 -> Sem r (ResultSet (Qualified ConvId)) + getRemotes lastRemote maxRemotes = tUntagged <$$> ConvStore.getRemoteConversationIds usr lastRemote maxRemotes + +-- | This function only exists because we use the 'MultiTablePage' type for the +-- endpoint. Since now the pagination is based on the qualified ids, we can +-- remove the use of this type in future API versions. +getConversationIdsImpl :: + forall r. + (Member ConversationStore r) => + Local UserId -> + Range 1 1000 Int32 -> + Maybe ConversationPagingState -> + Sem r ConvIdsPage +getConversationIdsImpl lusr maxIds pagingState = do + let mLastId = Aeson.decode . BS.fromStrict =<< (.mtpsState) =<< pagingState + resultSet <- getConversationIdsResultSet lusr maxIds mLastId + let mLastResult = lastMay resultSet.resultSetResult + pure + MultiTablePage + { mtpResults = resultSet.resultSetResult, + mtpHasMore = case resultSet.resultSetType of + ResultSetTruncated -> True + ResultSetComplete -> False, + mtpPagingState = + MultiTablePagingState + { mtpsTable = case fmap (flip relativeTo lusr) mLastResult of + Just (Local _) -> PagingLocals + Just (Remote _) -> PagingRemotes + Nothing -> PagingRemotes, + mtpsState = BS.toStrict . Aeson.encode <$> mLastResult + } + } diff --git a/services/galley/src/Galley/Cassandra/Store.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Internal.hs similarity index 51% rename from services/galley/src/Galley/Cassandra/Store.hs rename to libs/wire-subsystems/src/Wire/ConversationSubsystem/Internal.hs index 16794523557..68bf8c32a3d 100644 --- a/services/galley/src/Galley/Cassandra/Store.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Internal.hs @@ -1,6 +1,6 @@ -- This file is part of the Wire Server implementation. -- --- Copyright (C) 2022 Wire Swiss GmbH +-- Copyright (C) 2026 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,22 +15,27 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Galley.Cassandra.Store - ( embedClient, - ) -where +module Wire.ConversationSubsystem.Internal (internalGetClientIdsImpl) where -import Cassandra +import Data.Id +import Galley.Types.Clients (Clients, fromUserClients) import Imports import Polysemy import Polysemy.Input +import Wire.API.Conversation.Config +import Wire.BrigAPIAccess +import Wire.UserClientIndexStore (UserClientIndexStore) +import Wire.UserClientIndexStore qualified as UserClientIndexStore -embedClient :: - ( Member (Embed IO) r, - Member (Input ClientState) r +internalGetClientIdsImpl :: + ( Member BrigAPIAccess r, + Member UserClientIndexStore r, + Member (Input ConversationSubsystemConfig) r ) => - Client a -> - Sem r a -embedClient client = do - cs <- input - embed @IO $ runClient cs client + [UserId] -> + Sem r Clients +internalGetClientIdsImpl users = do + isInternal <- inputs (.listClientsUsingBrig) + if isInternal + then fromUserClients <$> lookupClients users + else UserClientIndexStore.getClients users diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs index 0fe59a7476b..9a3d9270bc4 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs @@ -1,3 +1,5 @@ +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2026 Wire Swiss GmbH @@ -20,77 +22,36 @@ module Wire.ConversationSubsystem.Interpreter ) where -import Control.Error (headMay) -import Control.Lens hiding ((??)) -import Data.Default -import Data.Id -import Data.Json.Util (ToJSONObject (toJSONObject)) -import Data.Misc (FutureWork (FutureWork)) -import Data.Qualified -import Data.Range -import Data.Set qualified as Set -import Data.Singletons (Sing) -import Data.UUID.Tagged qualified as U -import GHC.TypeNats -import Galley.Types.Clients (Clients, fromUserClients) import Galley.Types.Error (InternalError, InvalidInput (..)) import Imports -import Network.AMQP qualified as Q import Polysemy import Polysemy.Error import Polysemy.Input -import Wire.API.Conversation hiding (Member) -import Wire.API.Conversation qualified as Public -import Wire.API.Conversation.Action -import Wire.API.Conversation.CellsState import Wire.API.Conversation.Config -import Wire.API.Conversation.Role import Wire.API.Error import Wire.API.Error.Galley -import Wire.API.Event.Conversation -import Wire.API.Federation.API (makeConversationUpdateBundle, sendBundle) -import Wire.API.Federation.API.Galley.Notifications (ConversationUpdate (..)) import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Error -import Wire.API.History (History (HistoryPrivate)) -import Wire.API.Push.V2 qualified as PushV2 -import Wire.API.Team -import Wire.API.Team.Collaborator qualified as CollaboratorPermission -import Wire.API.Team.Feature -import Wire.API.Team.Feature qualified as Conf -import Wire.API.Team.FeatureFlags (notTeamMember) -import Wire.API.Team.LegalHold (LegalholdProtectee (LegalholdPlusFederationNotImplemented)) -import Wire.API.Team.Member -import Wire.API.Team.Permission hiding (self) -import Wire.API.User -import Wire.BackendNotificationQueueAccess (BackendNotificationQueueAccess, enqueueNotificationsConcurrently) +import Wire.BackendNotificationQueueAccess (BackendNotificationQueueAccess) import Wire.BrigAPIAccess import Wire.ConversationStore (ConversationStore) import Wire.ConversationStore qualified as ConvStore import Wire.ConversationSubsystem -import Wire.ConversationSubsystem qualified as ConversationSubsystem -import Wire.ConversationSubsystem.One2One -import Wire.ConversationSubsystem.Util +import Wire.ConversationSubsystem.CreateInternal qualified as CreateInternal +import Wire.ConversationSubsystem.Fetch qualified as Fetch +import Wire.ConversationSubsystem.Internal qualified as Internal +import Wire.ConversationSubsystem.Notify qualified as Notify import Wire.ExternalAccess (ExternalAccess) import Wire.FeaturesConfigSubsystem import Wire.FederationAPIAccess (FederationAPIAccess) import Wire.LegalHoldStore (LegalHoldStore) import Wire.NotificationSubsystem as NS import Wire.Sem.Now (Now) -import Wire.Sem.Now qualified as Now import Wire.Sem.Random (Random) -import Wire.Sem.Random qualified as Random -import Wire.StoredConversation hiding (convTeam, id_, localOne2OneConvId) -import Wire.StoredConversation as Data (NewConversation (..), convType) -import Wire.StoredConversation qualified as Data import Wire.TeamCollaboratorsSubsystem import Wire.TeamStore (TeamStore) -import Wire.TeamStore qualified as TeamStore import Wire.TeamSubsystem (TeamSubsystem) -import Wire.TeamSubsystem qualified as TeamSubsystem import Wire.UserClientIndexStore (UserClientIndexStore) -import Wire.UserClientIndexStore qualified as UserClientIndexStore -import Wire.UserList (UserList (UserList), toUserList, ulAddLocal, ulAll, ulFromLocals, ulLocals, ulRemotes) interpretConversationSubsystem :: ( Member (Error FederationError) r, @@ -132,691 +93,20 @@ interpretConversationSubsystem :: Sem r a interpretConversationSubsystem = interpret $ \case NotifyConversationAction tag quid notifyOrigDomain con lconv targetsLocal targetsRemote targetsBots action extraData -> - notifyConversationActionImpl tag quid notifyOrigDomain con lconv targetsLocal targetsRemote targetsBots action extraData - ConversationSubsystem.CreateGroupConversation lusr conn newConv -> - createGroupConversationGeneric lusr conn newConv - ConversationSubsystem.CreateOne2OneConversation lusr conn newOne2One -> - createOne2OneConversationLogic lusr conn newOne2One - ConversationSubsystem.CreateProteusSelfConversation lusr -> - createProteusSelfConversationLogic lusr - ConversationSubsystem.CreateConnectConversation lusr conn j -> - createConnectConversationLogic lusr conn j + Notify.notifyConversationActionImpl tag quid notifyOrigDomain con lconv targetsLocal targetsRemote targetsBots action extraData + CreateGroupConversation lusr conn newConv -> + CreateInternal.createGroupConversationGeneric lusr conn newConv + CreateOne2OneConversation lusr conn newOne2One -> + CreateInternal.createOne2OneConversationLogic lusr conn newOne2One + CreateProteusSelfConversation lusr -> + CreateInternal.createProteusSelfConversationLogic lusr + CreateConnectConversation lusr conn j -> + CreateInternal.createConnectConversationLogic lusr conn j + GetConversations convIds -> + ConvStore.getConversations convIds + GetConversationIds lusr maxIds pagingState -> + Fetch.getConversationIdsImpl lusr maxIds pagingState InternalGetClientIds uids -> - internalGetClientIdsImpl uids - ConversationSubsystem.InternalGetLocalMember cid uid -> + Internal.internalGetClientIdsImpl uids + InternalGetLocalMember cid uid -> ConvStore.getLocalMember cid uid - -createGroupConversationGeneric :: - forall r. - ( Member BrigAPIAccess r, - Member ConversationStore r, - Member (ErrorS 'ConvAccessDenied) r, - Member (Error InvalidInput) r, - Member (ErrorS 'NotATeamMember) r, - Member (ErrorS OperationDenied) r, - Member (ErrorS 'NotConnected) r, - Member (ErrorS 'MLSNotEnabled) r, - Member (ErrorS 'MLSNonEmptyMemberList) r, - Member (ErrorS 'MissingLegalholdConsent) r, - Member (ErrorS 'ChannelsNotEnabled) r, - Member (ErrorS 'NotAnMlsConversation) r, - Member (ErrorS HistoryNotSupported) r, - Member (Input ConversationSubsystemConfig) r, - Member LegalHoldStore r, - Member TeamStore r, - Member FeaturesConfigSubsystem r, - Member TeamCollaboratorsSubsystem r, - Member Random r, - Member TeamSubsystem r, - Member Now r, - Member NotificationSubsystem r, - Member (Error FederationError) r, - Member (Error UnreachableBackends) r, - Member BackendNotificationQueueAccess r, - Member (FederationAPIAccess FederatorClient) r - ) => - Local UserId -> - Maybe ConnId -> - Public.NewConv -> - Sem r StoredConversation -createGroupConversationGeneric lusr conn newConv = do - (nc, fromConvSize -> allUsers) <- newRegularConversation lusr newConv - checkCreateConvPermissions lusr newConv newConv.newConvTeam allUsers - ensureNoLegalholdConflicts allUsers - when (newConv.newConvHistory /= HistoryPrivate && newConv.newConvGroupConvType /= Channel) $ - throwS @HistoryNotSupported - - when (Public.newConvProtocol newConv == BaseProtocolMLSTag) $ do - assertMLSEnabled - - lcnv <- traverse (const Random.newId) lusr - storedConv <- createConversationImpl lcnv lusr nc - notifyConversationCreated lusr conn storedConv def - sendCellsNotification lusr conn storedConv - pure storedConv - -createOne2OneConversationLogic :: - ( Member BrigAPIAccess r, - Member ConversationStore r, - Member (Error FederationError) r, - Member (Error UnreachableBackends) r, - Member (Error InvalidInput) r, - Member (ErrorS 'NotATeamMember) r, - Member (ErrorS OperationDenied) r, - Member (ErrorS 'NonBindingTeam) r, - Member (ErrorS 'NoBindingTeamMembers) r, - Member (ErrorS 'TeamNotFound) r, - Member (ErrorS 'InvalidOperation) r, - Member (ErrorS 'NotConnected) r, - Member TeamStore r, - Member TeamCollaboratorsSubsystem r, - Member TeamSubsystem r, - Member Now r, - Member NotificationSubsystem r, - Member BackendNotificationQueueAccess r, - Member (FederationAPIAccess FederatorClient) r - ) => - Local UserId -> - ConnId -> - Public.NewOne2OneConv -> - Sem r (StoredConversation, Bool) -createOne2OneConversationLogic lusr zcon j = do - let allUsers = newOne2OneConvMembers lusr j - other <- ensureOne (ulAll lusr allUsers) - when (tUntagged lusr == other) $ - throwS @'InvalidOperation - mtid <- case j.team of - Just ti -> do - foldQualified - lusr - (\lother -> checkBindingTeamPermissions lusr lother (cnvTeamId ti)) - (const (pure Nothing)) - other - Nothing -> ensureConnected lusr allUsers $> Nothing - foldQualified - lusr - (createLegacyOne2OneConversationUnchecked lusr zcon j.name mtid) - (createOne2OneConversationUnchecked lusr zcon j.name mtid . tUntagged) - other - -createProteusSelfConversationLogic :: - (Member ConversationStore r) => - Local UserId -> - Sem r (StoredConversation, Bool) -createProteusSelfConversationLogic lusr = do - let lcnv = fmap Data.selfConv lusr - c <- ConvStore.getConversation (tUnqualified lcnv) - maybe (create lcnv) (\conv -> pure (conv, False)) c - where - create lcnv = do - let nc = - Data.NewConversation - { metadata = (defConversationMetadata (Just (tUnqualified lusr))) {cnvmType = Public.SelfConv}, - users = ulFromLocals [toUserRole (tUnqualified lusr)], - protocol = BaseProtocolProteusTag, - groupId = Nothing - } - conv <- createConversationImpl lcnv lusr nc - pure (conv, True) - -createConversationImpl :: - (Member ConversationStore r) => - Local ConvId -> - Local UserId -> - Data.NewConversation -> - Sem r StoredConversation -createConversationImpl lconv _lusr = - ConvStore.upsertConversation lconv - -createConnectConversationLogic :: - ( Member ConversationStore r, - Member (Error FederationError) r, - Member (Error InternalError) r, - Member (Error InvalidInput) r, - Member (Error UnreachableBackends) r, - Member (ErrorS 'ConvNotFound) r, - Member (ErrorS 'InvalidOperation) r, - Member NotificationSubsystem r, - Member BackendNotificationQueueAccess r, - Member Now r, - Member (FederationAPIAccess FederatorClient) r - ) => - Local UserId -> - Maybe ConnId -> - Connect -> - Sem r (StoredConversation, Bool) -createConnectConversationLogic lusr conn j = do - lrecipient <- ensureLocal lusr (cRecipient j) - n <- rangeCheckedMaybe (cName j) - let meta = - (defConversationMetadata (Just (tUnqualified lusr))) - { cnvmType = Public.ConnectConv, - cnvmName = fmap fromRange n - } - lcnv <- localOne2OneConvId lusr lrecipient - let nc = - Data.NewConversation - { -- We add only one member, second one gets added later, - -- when the other user accepts the connection request. - users = ulFromLocals [(toUserRole . tUnqualified) lusr], - protocol = BaseProtocolProteusTag, - metadata = meta, - groupId = Nothing - } - mconv <- ConvStore.getConversation (tUnqualified lcnv) - case mconv of - Nothing -> do - conv <- create lcnv nc - pure (conv, True) - Just conv -> do - conv' <- update n conv - pure (conv', False) - where - create lcnv nc = do - conv <- createConversationImpl lcnv lusr nc - notifyConversationCreated lusr conn conv def - notifyConversationUpdated lusr conn j conv - pure conv - update n conv = do - let mems = conv.localMembers - if tUnqualified lusr `isMember` mems - then -- we already were in the conversation, maybe also other - connect n conv - else do - let lcid = qualifyAs lusr conv.id_ - mm <- ConvStore.upsertMember lcid lusr - let conv' = - conv - { localMembers = conv.localMembers <> toList mm - } - if null mems - then -- the conversation was empty - connect n conv' - else do - -- we were not in the conversation, but someone else - conv'' <- acceptOne2One lusr conv' conn - if Data.convType conv'' == Public.ConnectConv - then connect n conv'' - else pure conv'' - connect n conv - | Data.convType conv == Public.ConnectConv = do - n' <- case n of - Just x -> do - ConvStore.setConversationName conv.id_ x - pure . Just $ fromRange x - Nothing -> pure $ Data.convName conv - notifyConversationUpdated lusr conn j conv - pure $ Data.convSetName n' conv - | otherwise = pure conv - -ensureNoLegalholdConflicts :: - ( Member (ErrorS 'MissingLegalholdConsent) r, - Member (Input ConversationSubsystemConfig) r, - Member LegalHoldStore r, - Member TeamStore r, - Member TeamSubsystem r - ) => - UserList UserId -> - Sem r () -ensureNoLegalholdConflicts (UserList locals remotes) = do - let FutureWork _remotes = FutureWork @'LegalholdPlusFederationNotImplemented remotes - whenM (anyLegalholdActivated locals) $ - unlessM (allLegalholdConsentGiven locals) $ - throwS @'MissingLegalholdConsent - -checkCreateConvPermissions :: - ( Member BrigAPIAccess r, - Member (ErrorS 'ConvAccessDenied) r, - Member (ErrorS 'NotATeamMember) r, - Member (ErrorS OperationDenied) r, - Member (ErrorS 'NotConnected) r, - Member (ErrorS 'ChannelsNotEnabled) r, - Member (ErrorS 'NotAnMlsConversation) r, - Member TeamStore r, - Member FeaturesConfigSubsystem r, - Member TeamCollaboratorsSubsystem r, - Member TeamSubsystem r - ) => - Local UserId -> - Public.NewConv -> - Maybe ConvTeamInfo -> - UserList UserId -> - Sem r () -checkCreateConvPermissions lusr newConv Nothing allUsers = do - when (newConv.newConvGroupConvType == Channel) $ throwS @OperationDenied - activated <- listToMaybe <$> lookupActivatedUsers [tUnqualified lusr] - void $ noteS @OperationDenied activated - tm <- getTeamMember (tUnqualified lusr) Nothing - for_ tm $ - permissionCheck AddRemoveConvMember . Just - ensureConnected lusr allUsers -checkCreateConvPermissions lusr newConv (Just tinfo) allUsers = do - let convTeam = cnvTeamId tinfo - mTeamMember <- getTeamMember (tUnqualified lusr) (Just convTeam) - teamAssociation <- case mTeamMember of - Just tm -> pure (Just (Right tm)) - Nothing -> do - Left <$$> internalGetTeamCollaborator convTeam (tUnqualified lusr) - - let checkGroup = do - void $ permissionCheck CreateConversation teamAssociation - when (length allUsers > 1 || Public.newConvProtocol newConv == BaseProtocolMLSTag) $ do - void $ permissionCheck AddRemoveConvMember teamAssociation - case newConv.newConvGroupConvType of - Channel -> do - ensureCreateChannelPermissions tinfo.cnvTeamId mTeamMember - GroupConversation -> checkGroup - MeetingConversation -> checkGroup - - convLocalMemberships <- mapM (flip TeamSubsystem.internalGetTeamMember convTeam) (ulLocals allUsers) - ensureAccessRole (accessRoles newConv) (zip (ulLocals allUsers) convLocalMemberships) - ensureConnectedToLocals (tUnqualified lusr) (notTeamMember (ulLocals allUsers) (catMaybes convLocalMemberships)) - ensureConnectedToRemotes lusr (ulRemotes allUsers) - where - ensureCreateChannelPermissions :: - forall r. - ( Member (ErrorS OperationDenied) r, - Member FeaturesConfigSubsystem r, - Member (ErrorS 'NotATeamMember) r, - Member (ErrorS 'ChannelsNotEnabled) r, - Member (ErrorS 'NotAnMlsConversation) r - ) => - TeamId -> - Maybe TeamMember -> - Sem r () - ensureCreateChannelPermissions tid (Just tm) = do - channelsConf :: LockableFeature ChannelsConfig <- getFeatureForTeam tid - when (channelsConf.status == FeatureStatusDisabled) $ throwS @'ChannelsNotEnabled - when (Public.newConvProtocol newConv /= BaseProtocolMLSTag) $ throwS @'NotAnMlsConversation - case channelsConf.config.allowedToCreateChannels of - Conf.Everyone -> pure () - Conf.TeamMembers -> void $ permissionCheck AddRemoveConvMember $ Just tm - Conf.Admins -> unless (isAdminOrOwner (tm ^. permissions)) $ throwS @OperationDenied - ensureCreateChannelPermissions _ Nothing = do - throwS @'NotATeamMember - -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 - -createLegacyOne2OneConversationUnchecked :: - ( Member ConversationStore r, - Member (Error FederationError) r, - Member (Error UnreachableBackends) r, - Member (Error InvalidInput) r, - Member Now r, - Member NotificationSubsystem r, - Member BackendNotificationQueueAccess r, - Member (FederationAPIAccess FederatorClient) r - ) => - Local UserId -> - ConnId -> - Maybe (Range 1 256 Text) -> - Maybe TeamId -> - Local UserId -> - Sem r (StoredConversation, Bool) -createLegacyOne2OneConversationUnchecked self zcon name mtid other = do - lcnv <- localOne2OneConvId self other - let meta = - (defConversationMetadata (Just (tUnqualified self))) - { cnvmType = Public.One2OneConv, - cnvmTeam = mtid, - cnvmName = fmap fromRange name - } - let nc = - Data.NewConversation - { users = ulFromLocals (map (toUserRole . tUnqualified) [self, other]), - protocol = BaseProtocolProteusTag, - metadata = meta, - groupId = Nothing - } - mc <- ConvStore.getConversation (tUnqualified lcnv) - case mc of - Just c -> pure (c, False) - Nothing -> do - conv <- createConversationImpl lcnv self nc - notifyConversationCreated self (Just zcon) conv def - pure (conv, True) - -createOne2OneConversationUnchecked :: - ( Member ConversationStore r, - Member (Error FederationError) r, - Member (Error UnreachableBackends) r, - Member Now r, - Member NotificationSubsystem r, - Member BackendNotificationQueueAccess r, - Member (FederationAPIAccess FederatorClient) r - ) => - Local UserId -> - ConnId -> - Maybe (Range 1 256 Text) -> - Maybe TeamId -> - Qualified UserId -> - Sem r (StoredConversation, Bool) -createOne2OneConversationUnchecked self zcon name mtid other = do - let create = - foldQualified - self - createOne2OneConversationLocally - createOne2OneConversationRemotely - create (one2OneConvId BaseProtocolProteusTag (tUntagged self) other) self zcon name mtid other - -createOne2OneConversationLocally :: - ( Member ConversationStore r, - Member (Error FederationError) r, - Member (Error UnreachableBackends) r, - Member Now r, - Member NotificationSubsystem r, - Member BackendNotificationQueueAccess r, - Member (FederationAPIAccess FederatorClient) r - ) => - Local ConvId -> - Local UserId -> - ConnId -> - Maybe (Range 1 256 Text) -> - Maybe TeamId -> - Qualified UserId -> - Sem r (StoredConversation, Bool) -createOne2OneConversationLocally lcnv self zcon name mtid other = do - mc <- ConvStore.getConversation (tUnqualified lcnv) - case mc of - Just c -> pure (c, False) - Nothing -> do - let meta = - (defConversationMetadata (Just (tUnqualified self))) - { cnvmType = Public.One2OneConv, - cnvmTeam = mtid, - cnvmName = fmap fromRange name - } - let nc = - Data.NewConversation - { metadata = meta, - users = fmap toUserRole (toUserList lcnv [tUntagged self, other]), - protocol = BaseProtocolProteusTag, - groupId = Nothing - } - conv <- createConversationImpl lcnv self nc - notifyConversationCreated self (Just zcon) conv def - pure (conv, True) - -createOne2OneConversationRemotely :: - (Member (Error FederationError) r) => - Remote ConvId -> - Local UserId -> - ConnId -> - Maybe (Range 1 256 Text) -> - Maybe TeamId -> - Qualified UserId -> - Sem r (StoredConversation, Bool) -createOne2OneConversationRemotely _ _ _ _name _mtid _ = - throw FederationNotImplemented - -newRegularConversation :: - ( Member (ErrorS 'MLSNonEmptyMemberList) r, - Member (ErrorS OperationDenied) r, - Member (Error InvalidInput) r, - Member (Input ConversationSubsystemConfig) r, - Member ConversationStore r - ) => - Local UserId -> - Public.NewConv -> - Sem r (Data.NewConversation, ConvSizeChecked UserList UserId) -newRegularConversation lusr newConv = do - cfg <- input - let uncheckedUsers = newConvMembers lusr newConv - forM_ newConv.newConvParent $ \parent -> do - mMembership <- ConvStore.getLocalMember parent (tUnqualified lusr) - when (isNothing mMembership) $ - throwS @OperationDenied - users <- case Public.newConvProtocol newConv of - BaseProtocolProteusTag -> checkedConvSize cfg uncheckedUsers - BaseProtocolMLSTag -> do - unless (null uncheckedUsers) $ throwS @'MLSNonEmptyMemberList - pure mempty - let usersWithoutCreator = (,newConvUsersRole newConv) <$> fromConvSize users - newConvUsersRoles = - if newConv.newConvSkipCreator - then usersWithoutCreator - else ulAddLocal (toUserRole (tUnqualified lusr)) usersWithoutCreator - let nc = - Data.NewConversation - { metadata = - Public.ConversationMetadata - { cnvmType = Public.RegularConv, - cnvmCreator = Just (tUnqualified lusr), - cnvmAccess = access newConv, - cnvmAccessRoles = accessRoles newConv, - cnvmName = fmap fromRange newConv.newConvName, - cnvmMessageTimer = newConv.newConvMessageTimer, - cnvmReceiptMode = case Public.newConvProtocol newConv of - BaseProtocolProteusTag -> newConv.newConvReceiptMode - BaseProtocolMLSTag -> Just def, - cnvmTeam = fmap cnvTeamId newConv.newConvTeam, - cnvmGroupConvType = Just newConv.newConvGroupConvType, - cnvmChannelAddPermission = if newConv.newConvGroupConvType == Channel then newConv.newConvChannelAddPermission <|> Just def else Nothing, - cnvmCellsState = - if newConv.newConvCells - then CellsPending - else CellsDisabled, - cnvmParent = newConv.newConvParent, - cnvmHistory = newConv.newConvHistory - }, - users = newConvUsersRoles, - protocol = Public.newConvProtocol newConv, - groupId = Nothing - } - pure (nc, users) - -localOne2OneConvId :: - (Member (Error InvalidInput) r) => - Local UserId -> - Local UserId -> - Sem r (Local ConvId) -localOne2OneConvId self other = do - (x, y) <- toUUIDs (tUnqualified self) (tUnqualified other) - pure . qualifyAs self $ Data.localOne2OneConvId x y - where - toUUIDs :: - (Member (Error InvalidInput) r) => - UserId -> - UserId -> - Sem r (U.UUID U.V4, U.UUID U.V4) - toUUIDs a b = do - a' <- U.fromUUID (toUUID a) & note InvalidUUID4 - b' <- U.fromUUID (toUUID b) & note InvalidUUID4 - pure (a', b') - -accessRoles :: Public.NewConv -> Set AccessRole -accessRoles b = fromMaybe defRole (newConvAccessRoles b) - -access :: Public.NewConv -> [Access] -access a = case Set.toList (Public.newConvAccess a) of - [] -> Data.defRegularConvAccess - (x : xs) -> x : xs - -newConvMembers :: Local x -> Public.NewConv -> UserList UserId -newConvMembers loc body = - UserList (newConvUsers body) [] - <> toUserList loc (newConvQualifiedUsers body) - -newOne2OneConvMembers :: Local x -> Public.NewOne2OneConv -> UserList UserId -newOne2OneConvMembers loc body = - UserList body.users [] - <> toUserList loc body.qualifiedUsers - -ensureOne :: (Member (Error InvalidInput) r) => [a] -> Sem r a -ensureOne [x] = pure x -ensureOne _ = throw (InvalidRange "One-to-one conversations can only have a single invited member") - -assertMLSEnabled :: (Member (Input ConversationSubsystemConfig) r, Member (ErrorS 'MLSNotEnabled) r) => Sem r () -assertMLSEnabled = do - cfg <- input - when (null cfg.mlsKeys) $ throwS @'MLSNotEnabled - -newtype ConvSizeChecked f a = ConvSizeChecked {fromConvSize :: f a} - deriving (Functor, Foldable, Traversable) - deriving newtype (Semigroup, Monoid) - -checkedConvSize :: - (Member (Error InvalidInput) r, Foldable f) => - ConversationSubsystemConfig -> - f a -> - Sem r (ConvSizeChecked f a) -checkedConvSize cfg x = do - let minV :: Integer = 0 - limit = cfg.maxConvSize - 1 - if length x <= fromIntegral limit - then pure (ConvSizeChecked x) - else throwErr (errorMsg minV limit "") - -rangeChecked :: (KnownNat n, KnownNat m, Member (Error InvalidInput) r, Within a n m) => a -> Sem r (Range n m a) -rangeChecked = either throwErr pure . checkedEither -{-# INLINE rangeChecked #-} - -rangeCheckedMaybe :: - (Member (Error InvalidInput) r, KnownNat n, KnownNat m, Within a n m) => - Maybe a -> - Sem r (Maybe (Range n m a)) -rangeCheckedMaybe Nothing = pure Nothing -rangeCheckedMaybe (Just a) = Just <$> rangeChecked a -{-# INLINE rangeCheckedMaybe #-} - -throwErr :: (Member (Error InvalidInput) r) => String -> Sem r a -throwErr = throw . InvalidRange . fromString - -checkBindingTeamPermissions :: - ( Member (ErrorS 'NoBindingTeamMembers) r, - Member (ErrorS 'NonBindingTeam) r, - Member (ErrorS 'NotATeamMember) r, - Member (ErrorS OperationDenied) r, - Member (ErrorS 'TeamNotFound) r, - Member TeamCollaboratorsSubsystem r, - Member TeamStore r, - Member TeamSubsystem r - ) => - Local UserId -> - Local UserId -> - TeamId -> - Sem r (Maybe TeamId) -checkBindingTeamPermissions lusr lother tid = do - mTeamCollaborator <- internalGetTeamCollaborator 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 - TeamStore.getTeamBinding tid >>= \case - Just Binding -> do - when (isJust zusrMembership) $ - verifyMembership tid (tUnqualified lusr) - mOtherTeamCollaborator <- internalGetTeamCollaborator tid (tUnqualified lother) - unless (isJust mOtherTeamCollaborator) $ - verifyMembership tid (tUnqualified lother) - pure (Just tid) - Just _ -> throwS @'NonBindingTeam - Nothing -> throwS @'TeamNotFound - where - guardPerm p m = - if m `hasPermission` p - then pure () - else throwS @OperationDenied - -verifyMembership :: - ( Member (ErrorS 'NoBindingTeamMembers) r, - Member TeamSubsystem r - ) => - TeamId -> - UserId -> - Sem r () -verifyMembership tid u = do - membership <- TeamSubsystem.internalGetTeamMember u tid - when (isNothing membership) $ - throwS @'NoBindingTeamMembers - -sendCellsNotification :: - ( Member NotificationSubsystem r, - Member Now r - ) => - Local UserId -> - Maybe ConnId -> - StoredConversation -> - Sem r () -sendCellsNotification lusr conn conv = do - now <- Now.get - let lconv = qualifyAs lusr conv.id_ - event = CellsEvent (tUntagged lconv) (tUntagged lusr) now CellsConvCreateNoData - when (conv.metadata.cnvmCellsState /= CellsDisabled) $ do - let push = - def - { origin = Just (tUnqualified lusr), - json = toJSONObject event, - isCellsEvent = True, - route = PushV2.RouteAny, - conn - } - NS.pushNotifications [push] - -notifyConversationActionImpl :: - forall tag r. - ( Member BackendNotificationQueueAccess r, - Member ExternalAccess r, - Member (Error FederationError) r, - Member Now r, - Member NotificationSubsystem r - ) => - Sing tag -> - EventFrom -> - Bool -> - Maybe ConnId -> - Local StoredConversation -> - Set UserId -> - Set (Remote UserId) -> - Set BotMember -> - ConversationAction (tag :: ConversationActionTag) -> - Public.ExtraConversationData -> - Sem r LocalConversationUpdate -notifyConversationActionImpl tag eventFrom notifyOrigDomain con lconv targetsLocal targetsRemote targetsBots action extraData = do - now <- Now.get - let lcnv = fmap (.id_) lconv - conv = tUnqualified lconv - tid = conv.metadata.cnvmTeam - e = conversationActionToEvent tag now eventFrom (tUntagged lcnv) extraData Nothing tid action - quid = eventFromUserId eventFrom - mkUpdate uids = - ConversationUpdate - { time = now, - origUserId = quid, - convId = tUnqualified lcnv, - alreadyPresentUsers = uids, - action = SomeConversationAction tag action, - extraConversationData = Just extraData - } - update <- - fmap (fromMaybe (mkUpdate []) . asum . map tUnqualified) $ - enqueueNotificationsConcurrently Q.Persistent (toList targetsRemote) $ - \ruids -> do - let update = mkUpdate (tUnqualified ruids) - if notifyOrigDomain || tDomain ruids /= qDomain quid - then do - makeConversationUpdateBundle update >>= sendBundle - pure Nothing - else pure (Just update) - - pushConversationEvent con conv.metadata.cnvmCellsState e (qualifyAs lcnv targetsLocal) targetsBots - - pure $ LocalConversationUpdate {lcuEvent = e, lcuUpdate = update} - -internalGetClientIdsImpl :: - ( Member BrigAPIAccess r, - Member UserClientIndexStore r, - Member (Input ConversationSubsystemConfig) r - ) => - [UserId] -> - Sem r Clients -internalGetClientIdsImpl users = do - isInternal <- inputs (.listClientsUsingBrig) - if isInternal - then fromUserClients <$> lookupClients users - else UserClientIndexStore.getClients users diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Notify.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Notify.hs new file mode 100644 index 00000000000..a02de14f9aa --- /dev/null +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Notify.hs @@ -0,0 +1,91 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2026 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.ConversationSubsystem.Notify (notifyConversationActionImpl) where + +import Data.Id +import Data.Qualified +import Data.Singletons (Sing) +import Imports +import Network.AMQP qualified as Q +import Polysemy +import Polysemy.Error +import Wire.API.Conversation hiding (Member) +import Wire.API.Conversation qualified as Public +import Wire.API.Conversation.Action +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 +import Wire.BackendNotificationQueueAccess (BackendNotificationQueueAccess, enqueueNotificationsConcurrently) +import Wire.ConversationSubsystem.Util +import Wire.ExternalAccess (ExternalAccess) +import Wire.NotificationSubsystem as NS +import Wire.Sem.Now (Now) +import Wire.Sem.Now qualified as Now +import Wire.StoredConversation hiding (convTeam, id_, localOne2OneConvId) +import Wire.StoredConversation qualified as Data + +notifyConversationActionImpl :: + forall tag r. + ( Member BackendNotificationQueueAccess r, + Member ExternalAccess r, + Member (Error FederationError) r, + Member Now r, + Member NotificationSubsystem r + ) => + Sing tag -> + EventFrom -> + Bool -> + Maybe ConnId -> + Local StoredConversation -> + Set UserId -> + Set (Remote UserId) -> + Set BotMember -> + ConversationAction (tag :: ConversationActionTag) -> + Public.ExtraConversationData -> + Sem r LocalConversationUpdate +notifyConversationActionImpl tag eventFrom notifyOrigDomain con lconv targetsLocal targetsRemote targetsBots action extraData = do + now <- Now.get + let lcnv = fmap (.id_) lconv + conv = tUnqualified lconv + tid = conv.metadata.cnvmTeam + e = conversationActionToEvent tag now eventFrom (tUntagged lcnv) extraData Nothing tid action + quid = eventFromUserId eventFrom + mkUpdate uids = + ConversationUpdate + { time = now, + origUserId = quid, + convId = tUnqualified lcnv, + alreadyPresentUsers = uids, + action = SomeConversationAction tag action, + extraConversationData = Just extraData + } + update <- + fmap (fromMaybe (mkUpdate []) . asum . map tUnqualified) $ + enqueueNotificationsConcurrently Q.Persistent (toList targetsRemote) $ + \ruids -> do + let update = mkUpdate (tUnqualified ruids) + if notifyOrigDomain || tDomain ruids /= qDomain quid + then do + makeConversationUpdateBundle update >>= sendBundle + pure Nothing + else pure (Just update) + + pushConversationEvent con conv.metadata.cnvmCellsState e (qualifyAs lcnv targetsLocal) targetsBots + + pure $ LocalConversationUpdate {lcuEvent = e, lcuUpdate = update} diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Util.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Util.hs index 0654caed4f7..1d044ab5f2e 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Util.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Util.hs @@ -96,7 +96,6 @@ 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 @@ -162,7 +161,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) <$> TeamSubsytem.internalSelectTeamMembers team uids + fmap (view Mem.userId) <$> TeamSubsystem.internalSelectTeamMembers team uids -- Do not check connections for users that are on the same team ensureConnectedToLocals u ((uids \\ join sameTeamUids) \\ icUsers) where diff --git a/services/galley/src/Galley/Effects/CustomBackendStore.hs b/libs/wire-subsystems/src/Wire/CustomBackendStore.hs similarity index 96% rename from services/galley/src/Galley/Effects/CustomBackendStore.hs rename to libs/wire-subsystems/src/Wire/CustomBackendStore.hs index 9e55e749431..cf564e66505 100644 --- a/services/galley/src/Galley/Effects/CustomBackendStore.hs +++ b/libs/wire-subsystems/src/Wire/CustomBackendStore.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.CustomBackendStore +module Wire.CustomBackendStore ( CustomBackendStore (..), getCustomBackend, setCustomBackend, diff --git a/services/galley/src/Galley/Cassandra/CustomBackend.hs b/libs/wire-subsystems/src/Wire/CustomBackendStore/Cassandra.hs similarity index 67% rename from services/galley/src/Galley/Cassandra/CustomBackend.hs rename to libs/wire-subsystems/src/Wire/CustomBackendStore/Cassandra.hs index 6cbec51e997..4128c2cee62 100644 --- a/services/galley/src/Galley/Cassandra/CustomBackend.hs +++ b/libs/wire-subsystems/src/Wire/CustomBackendStore/Cassandra.hs @@ -17,20 +17,19 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Galley.Cassandra.CustomBackend (interpretCustomBackendStoreToCassandra) where +module Wire.CustomBackendStore.Cassandra (interpretCustomBackendStoreToCassandra) where import Cassandra import Data.Domain (Domain) -import Galley.Cassandra.Queries qualified as Cql -import Galley.Cassandra.Store -import Galley.Cassandra.Util -import Galley.Effects.CustomBackendStore (CustomBackendStore (..)) +import Data.Misc import Imports import Polysemy import Polysemy.Input import Polysemy.TinyLog import Wire.API.CustomBackend import Wire.ConversationStore.Cassandra.Instances () +import Wire.CustomBackendStore (CustomBackendStore (..)) +import Wire.Util interpretCustomBackendStoreToCassandra :: ( Member (Embed IO) r, @@ -42,26 +41,34 @@ interpretCustomBackendStoreToCassandra :: interpretCustomBackendStoreToCassandra = interpret $ \case GetCustomBackend dom -> do logEffect "CustomBackendStore.GetCustomBackend" - embedClient $ getCustomBackend dom + embedClientInput $ getCustomBackend dom SetCustomBackend dom b -> do logEffect "CustomBackendStore.SetCustomBackend" - embedClient $ setCustomBackend dom b + embedClientInput $ setCustomBackend dom b DeleteCustomBackend dom -> do logEffect "CustomBackendStore.DeleteCustomBackend" - embedClient $ deleteCustomBackend dom + embedClientInput $ deleteCustomBackend dom getCustomBackend :: (MonadClient m) => Domain -> m (Maybe CustomBackend) getCustomBackend domain = fmap toCustomBackend <$> do - retry x1 $ query1 Cql.selectCustomBackend (params LocalQuorum (Identity domain)) + retry x1 $ query1 q (params LocalQuorum (Identity domain)) where toCustomBackend (backendConfigJsonUrl, backendWebappWelcomeUrl) = CustomBackend {..} + q :: PrepQuery R (Identity Domain) (HttpsUrl, HttpsUrl) + q = "select config_json_url, webapp_welcome_url from custom_backend where domain = ?" setCustomBackend :: (MonadClient m) => Domain -> CustomBackend -> m () setCustomBackend domain CustomBackend {..} = do - retry x5 $ write Cql.upsertCustomBackend (params LocalQuorum (backendConfigJsonUrl, backendWebappWelcomeUrl, domain)) + retry x5 $ write q (params LocalQuorum (backendConfigJsonUrl, backendWebappWelcomeUrl, domain)) + where + q :: PrepQuery W (HttpsUrl, HttpsUrl, Domain) () + q = "update custom_backend set config_json_url = ?, webapp_welcome_url = ? where domain = ?" deleteCustomBackend :: (MonadClient m) => Domain -> m () deleteCustomBackend domain = do - retry x5 $ write Cql.deleteCustomBackend (params LocalQuorum (Identity domain)) + retry x5 $ write q (params LocalQuorum (Identity domain)) + where + q :: PrepQuery W (Identity Domain) () + q = "delete from custom_backend where domain = ?" diff --git a/libs/wire-subsystems/src/Wire/FeaturesConfigSubsystem/Types.hs b/libs/wire-subsystems/src/Wire/FeaturesConfigSubsystem/Types.hs index d581888dd0f..4a13a8947b1 100644 --- a/libs/wire-subsystems/src/Wire/FeaturesConfigSubsystem/Types.hs +++ b/libs/wire-subsystems/src/Wire/FeaturesConfigSubsystem/Types.hs @@ -71,7 +71,7 @@ instance GetFeatureConfig SSOConfig instance GetFeatureConfig SearchVisibilityAvailableConfig -instance GetFeatureConfig ValidateSAMLEmailsConfig +instance GetFeatureConfig RequireExternalEmailVerificationConfig instance GetFeatureConfig DigitalSignaturesConfig diff --git a/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs b/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs index 1fac414fb65..37e2e0ebacc 100644 --- a/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs +++ b/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs @@ -110,7 +110,7 @@ data GalleyAPIAccess m a where TeamId -> GalleyAPIAccess m (LockableFeature LegalholdConfig) GetUserLegalholdStatus :: - Local UserId -> TeamId -> GalleyAPIAccess m UserLegalHoldStatusResponse + Local UserId -> TeamId -> GalleyAPIAccess m (Maybe UserLegalHoldStatusResponse) GetTeamSearchVisibility :: TeamId -> GalleyAPIAccess m TeamSearchVisibility diff --git a/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs b/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs index 38e54e5c303..026a5c7b35f 100644 --- a/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs +++ b/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs @@ -112,16 +112,19 @@ getUserLegalholdStatus :: ) => Local UserId -> TeamId -> - Sem (Input Endpoint : r) UserLegalHoldStatusResponse + Sem (Input Endpoint : r) (Maybe UserLegalHoldStatusResponse) getUserLegalholdStatus luid tid = do debug $ remote "galley" . msg (val "get legalhold user status") - decodeBodyOrThrow "galley" =<< galleyRequest do + rs <- galleyRequest do method GET . paths ["teams", toByteString' tid, "legalhold", toByteString' (tUnqualified luid)] . zUser (tUnqualified luid) - . expect2xx + . expect2xxOr404 + case Bilge.statusCode rs of + 200 -> Just <$> decodeBodyOrThrow "galley" rs + _ -> pure Nothing galleyRequest :: (Member Rpc r, Member (Input Endpoint) r) => (Request -> Request) -> Sem r (Response (Maybe LByteString)) galleyRequest req = do diff --git a/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs b/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs index f66cf80bb63..ed5de07f65c 100644 --- a/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs +++ b/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs @@ -20,7 +20,6 @@ module Wire.IndexedUserStore.Bulk.ElasticSearch where import Cassandra.Exec (paginateWithStateC) import Cassandra.Util (Writetime (Writetime)) import Conduit (ConduitT, runConduit, (.|)) -import Data.Aeson (encode) import Data.Conduit.Combinators qualified as Conduit import Data.Id import Data.Json.Util (UTCTimeMillis (fromUTCTimeMillis)) @@ -35,8 +34,6 @@ import System.Logger.Message qualified as Log import Wire.API.Team.Feature import Wire.API.Team.Member.Info import Wire.API.Team.Role -import Wire.API.User -import Wire.AppStore import Wire.GalleyAPIAccess import Wire.IndexedUserStore (IndexedUserStore) import Wire.IndexedUserStore qualified as IndexedUserStore @@ -52,7 +49,6 @@ import Wire.UserStore.IndexUser interpretIndexedUserStoreBulk :: ( Member TinyLog r, Member UserStore r, - Member AppStore r, Member (Concurrency Unsafe) r, Member GalleyAPIAccess r, Member IndexedUserStore r, @@ -68,7 +64,6 @@ interpretIndexedUserStoreBulk = interpret \case syncAllUsersImpl :: forall r. ( Member UserStore r, - Member AppStore r, Member TinyLog r, Member (Concurrency 'Unsafe) r, Member GalleyAPIAccess r, @@ -80,7 +75,6 @@ syncAllUsersImpl = syncAllUsersWithVersion ES.ExternalGT forceSyncAllUsersImpl :: forall r. ( Member UserStore r, - Member AppStore r, Member TinyLog r, Member (Concurrency 'Unsafe) r, Member GalleyAPIAccess r, @@ -92,7 +86,6 @@ forceSyncAllUsersImpl = syncAllUsersWithVersion ES.ExternalGTE syncAllUsersWithVersion :: forall r. ( Member UserStore r, - Member AppStore r, Member TinyLog r, Member (Concurrency 'Unsafe) r, Member GalleyAPIAccess r, @@ -126,9 +119,6 @@ syncAllUsersWithVersion mkVersion = teamIds = Map.keys teams visMap <- fmap Map.fromList . unsafePooledForConcurrentlyN 16 teamIds $ \t -> (t,) <$> teamSearchVisibilityInbound t - userTypes :: Map UserId UserType <- fmap Map.fromList . unsafePooledForConcurrentlyN 16 page $ \iu -> - (iu.userId,) <$> getUserType iu - warnIfMissingUserTypes page userTypes roles :: Map UserId (WithWritetime Role) <- fmap (Map.fromList . concat) . unsafePooledForConcurrentlyN 16 (Map.toList teams) $ \(t, us) -> do tms <- (.members) <$> selectTeamMemberInfos t (fmap (.userId) us) pure $ mapMaybe mkRoleWithWriteTime tms @@ -136,7 +126,6 @@ syncAllUsersWithVersion mkVersion = mkUserDoc indexUser = indexUserToDoc (vis indexUser) - (Map.lookup indexUser.userId userTypes) ((.value) <$> Map.lookup indexUser.userId roles) indexUser mkDocVersion u = mkVersion . ES.ExternalDocVersion . docVersion $ indexUserToVersion (Map.lookup u.userId roles) u @@ -154,29 +143,11 @@ syncAllUsersWithVersion mkVersion = ) <$> permissionsToRole tmi.permissions - -- `page` and `userTypes` *should* overlap perfectly, but we're - -- using `unsafePooledForConcurrentlyN` to make concurrent db - -- calls and that swallows any errors that might occur. - -- - -- FUTUREWORK: we need to get rid of `Wire.Sem.Concurrency`, it's - -- unidiomatic and dangerous! - warnIfMissingUserTypes :: [IndexUser] -> Map UserId ignored -> Sem r () - warnIfMissingUserTypes page userTypes = do - let missing = us \\ ts - us, ts :: [UserId] - us = (.userId) <$> page - ts = Map.keys userTypes - unless (null missing) do - warn $ - Log.field "missing" (encode missing) - . Log.msg (Log.val "Reindex: could not lookup all user types!") - migrateDataImpl :: ( Member IndexedUserStore r, Member (Error MigrationException) r, Member IndexedUserMigrationStore r, Member UserStore r, - Member AppStore r, Member (Concurrency Unsafe) r, Member GalleyAPIAccess r, Member TinyLog r @@ -205,18 +176,3 @@ teamSearchVisibilityInbound :: (Member GalleyAPIAccess r) => TeamId -> Sem r Sea teamSearchVisibilityInbound tid = searchVisibilityInboundFromFeatureStatus . (.status) <$> getFeatureConfigForTeam @_ @SearchVisibilityInboundConfig tid - --- | FUTUREWORK: this is duplicated code from UserSubsystem, we should --- probably expose it as an action there. -getUserType :: - forall r. - (Member AppStore r) => - IndexUser -> - Sem r UserType -getUserType iu = case iu.serviceId of - Just _ -> pure UserTypeBot - Nothing -> do - mmApp <- mapM (getApp iu.userId) iu.teamId - case join mmApp of - Just _ -> pure UserTypeApp - Nothing -> pure UserTypeRegular diff --git a/libs/wire-subsystems/src/Wire/ListItems/Team/Cassandra.hs b/libs/wire-subsystems/src/Wire/ListItems/Team/Cassandra.hs new file mode 100644 index 00000000000..682a5ea2d6f --- /dev/null +++ b/libs/wire-subsystems/src/Wire/ListItems/Team/Cassandra.hs @@ -0,0 +1,76 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2026 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.ListItems.Team.Cassandra + ( interpretTeamListToCassandra, + interpretInternalTeamListToCassandra, + ) +where + +import Cassandra +import Data.Id +import Data.Range +import Imports hiding (Set, max) +import Polysemy +import Polysemy.Input +import Polysemy.TinyLog +import Wire.ListItems +import Wire.Sem.Paging.Cassandra +import Wire.TeamStore.Cassandra.Queries qualified as Cql +import Wire.Util (embedClientInput, logEffect) + +interpretTeamListToCassandra :: + ( Member (Embed IO) r, + Member (Input ClientState) r, + Member TinyLog r + ) => + Sem (ListItems LegacyPaging TeamId ': r) a -> + Sem r a +interpretTeamListToCassandra = interpret $ \case + ListItems uid ps lim -> do + logEffect "TeamList.ListItems" + embedClientInput $ teamIdsFrom uid ps lim + +interpretInternalTeamListToCassandra :: + ( Member (Embed IO) r, + Member (Input ClientState) r, + Member TinyLog r + ) => + Sem (ListItems InternalPaging TeamId ': r) a -> + Sem r a +interpretInternalTeamListToCassandra = interpret $ \case + ListItems uid mps lim -> do + logEffect "InternalTeamList.ListItems" + embedClientInput $ case mps of + Nothing -> do + page <- teamIdsForPagination uid Nothing lim + mkInternalPage page pure + Just ps -> ipNext ps + +teamIdsFrom :: UserId -> Maybe TeamId -> Range 1 100 Int32 -> Client (ResultSet TeamId) +teamIdsFrom usr range (fromRange -> max) = + mkResultSet . fmap runIdentity . strip <$> case range of + Just c -> paginate Cql.selectUserTeamsFrom (paramsP LocalQuorum (usr, c) (max + 1)) + Nothing -> paginate Cql.selectUserTeams (paramsP LocalQuorum (Identity usr) (max + 1)) + where + strip p = p {result = take (fromIntegral max) (result p)} + +teamIdsForPagination :: UserId -> Maybe TeamId -> Range 1 100 Int32 -> Client (Page TeamId) +teamIdsForPagination usr range (fromRange -> max) = + fmap runIdentity <$> case range of + Just c -> paginate Cql.selectUserTeamsFrom (paramsP LocalQuorum (usr, c) max) + Nothing -> paginate Cql.selectUserTeams (paramsP LocalQuorum (Identity usr) max) diff --git a/libs/wire-subsystems/src/Wire/MeetingsStore.hs b/libs/wire-subsystems/src/Wire/MeetingsStore.hs index 581ce53c6e8..1c3a5839f31 100644 --- a/libs/wire-subsystems/src/Wire/MeetingsStore.hs +++ b/libs/wire-subsystems/src/Wire/MeetingsStore.hs @@ -148,5 +148,21 @@ data MeetingsStore m a where GetMeeting :: MeetingId -> MeetingsStore m (Maybe StoredMeeting) + ListMeetingsByUser :: + UserId -> + UTCTime -> + MeetingsStore m [StoredMeeting] + ListMeetingsByConversation :: + ConvId -> + UTCTime -> + MeetingsStore m [StoredMeeting] + AddInvitedEmails :: + MeetingId -> + [EmailAddress] -> + MeetingsStore m () + RemoveInvitedEmails :: + MeetingId -> + [EmailAddress] -> + MeetingsStore m () makeSem ''MeetingsStore diff --git a/libs/wire-subsystems/src/Wire/MeetingsStore/Postgres.hs b/libs/wire-subsystems/src/Wire/MeetingsStore/Postgres.hs index f72c0514a51..e8c1068c7f4 100644 --- a/libs/wire-subsystems/src/Wire/MeetingsStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/MeetingsStore/Postgres.hs @@ -29,17 +29,18 @@ import Data.Profunctor (dimap) import Data.Range (Range, fromRange) import Data.Time.Clock import Data.UUID (UUID, nil) +import Data.Vector qualified as V import Hasql.Pool import Hasql.Session import Hasql.Statement import Hasql.TH import Imports import Polysemy -import Polysemy.Error (Error, throw) +import Polysemy.Error (throw) import Polysemy.Input import Wire.API.Meeting (Recurrence) import Wire.API.PostgresMarshall (PostgresMarshall (..), PostgresUnmarshall (..), dimapPG) -import Wire.API.User.Identity (EmailAddress) +import Wire.API.User.Identity (EmailAddress, fromEmail) import Wire.MeetingsStore import Wire.Postgres (PGConstraints) @@ -54,6 +55,14 @@ interpretMeetingsStoreToPostgres = updateMeetingImpl meetingId title startDate endDate schedule GetMeeting meetingId -> getMeetingImpl meetingId + ListMeetingsByUser userId cutoffTime -> + listMeetingsByUserImpl userId cutoffTime + ListMeetingsByConversation convId cutoffTime -> + listMeetingsByConversationImpl convId cutoffTime + AddInvitedEmails meetingId email -> + addInvitedEmailsImpl meetingId email + RemoveInvitedEmails meetingId emails -> + removeInvitedEmailsImpl meetingId emails -- * Create @@ -168,10 +177,7 @@ instance {-# OVERLAPPING #-} PostgresMarshall UpdateStoredMeetingWithoutRecurren ) updateMeetingImpl :: - ( Member (Input Pool) r, - Member (Embed IO) r, - Member (Error UsageError) r - ) => + (PGConstraints r) => MeetingId -> Maybe (Range 1 256 Text) -> Maybe UTCTime -> @@ -263,3 +269,104 @@ getMeetingStatement = FROM meetings WHERE id = $1 :: uuid |] + +-- * List + +listMeetingsByUserImpl :: + (PGConstraints r) => + UserId -> + UTCTime -> + Sem r [StoredMeeting] +listMeetingsByUserImpl userId cutoffTime = do + pool <- input + result <- liftIO $ use pool session + either throw pure result + where + session :: Session [StoredMeeting] + session = statement (toUUID userId, cutoffTime) $ V.toList <$> listStatement + listStatement :: Statement (UUID, UTCTime) (V.Vector StoredMeeting) + listStatement = + refineResult + (traverse (postgresUnmarshall @StoredMeetingTuple @StoredMeeting)) + $ [vectorStatement| + SELECT + id :: uuid, title :: text, creator :: uuid, + start_time :: timestamptz, end_time :: timestamptz, + recurrence_frequency :: text?, recurrence_interval :: int4?, recurrence_until :: timestamptz?, + conversation_id :: uuid, invited_emails :: text[], trial :: boolean, + created_at :: timestamptz, updated_at :: timestamptz + FROM meetings + WHERE creator = ($1 :: uuid) AND end_time >= ($2 :: timestamptz) + ORDER BY start_time ASC + |] + +listMeetingsByConversationImpl :: + (PGConstraints r) => + ConvId -> + UTCTime -> + Sem r [StoredMeeting] +listMeetingsByConversationImpl convId cutoffTime = do + pool <- input + result <- liftIO $ use pool session + either throw pure result + where + session :: Session [StoredMeeting] + session = statement (toUUID convId, cutoffTime) $ V.toList <$> listStatement + listStatement :: Statement (UUID, UTCTime) (V.Vector StoredMeeting) + listStatement = + refineResult + (traverse (postgresUnmarshall @StoredMeetingTuple @StoredMeeting)) + $ [vectorStatement| + SELECT + id :: uuid, title :: text, creator :: uuid, + start_time :: timestamptz, end_time :: timestamptz, + recurrence_frequency :: text?, recurrence_interval :: int4?, recurrence_until :: timestamptz?, + conversation_id :: uuid, invited_emails :: text[], trial :: boolean, + created_at :: timestamptz, updated_at :: timestamptz + FROM meetings + WHERE conversation_id = ($1 :: uuid) AND end_time >= ($2 :: timestamptz) + ORDER BY start_time ASC + |] + +addInvitedEmailsImpl :: + (PGConstraints r) => + MeetingId -> + [EmailAddress] -> + Sem r () +addInvitedEmailsImpl meetingId emails = do + pool <- input + result <- liftIO $ use pool session + either throw pure result + where + session :: Session () + session = statement (V.fromList (fromEmail <$> emails), toUUID meetingId) addEmailStatement + + addEmailStatement :: Statement (V.Vector Text, UUID) () + addEmailStatement = + [resultlessStatement| + UPDATE meetings + SET invited_emails = array(SELECT DISTINCT unnest(array_cat(invited_emails, $1 :: text[]))), + updated_at = NOW() + WHERE id = ($2 :: uuid) + |] + +removeInvitedEmailsImpl :: + (PGConstraints r) => + MeetingId -> + [EmailAddress] -> + Sem r () +removeInvitedEmailsImpl meetingId emails = do + pool <- input + result <- liftIO $ use pool session + either throw pure result + where + session :: Session () + session = statement (V.fromList (fromEmail <$> emails), toUUID meetingId) removeEmailStatement + removeEmailStatement :: Statement (V.Vector Text, UUID) () + removeEmailStatement = + [resultlessStatement| + UPDATE meetings M + SET invited_emails = (SELECT array(SELECT unnest(M.invited_emails) EXCEPT SELECT unnest($1 :: text[]))), + updated_at = NOW() + WHERE id = ($2 :: uuid) + |] diff --git a/libs/wire-subsystems/src/Wire/MeetingsSubsystem.hs b/libs/wire-subsystems/src/Wire/MeetingsSubsystem.hs index f353a85a2d1..0aaf4a02883 100644 --- a/libs/wire-subsystems/src/Wire/MeetingsSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/MeetingsSubsystem.hs @@ -24,6 +24,7 @@ import Data.Qualified import Imports import Polysemy import Wire.API.Meeting +import Wire.API.User.EmailAddress (EmailAddress) import Wire.StoredConversation (StoredConversation) data MeetingsSubsystem m a where @@ -40,5 +41,18 @@ data MeetingsSubsystem m a where Local UserId -> Qualified MeetingId -> MeetingsSubsystem m (Maybe Meeting) + ListMeetings :: + Local UserId -> + MeetingsSubsystem m [Meeting] + AddInvitedEmails :: + Local UserId -> + Qualified MeetingId -> + [EmailAddress] -> + MeetingsSubsystem m Bool + RemoveInvitedEmails :: + Local UserId -> + Qualified MeetingId -> + [EmailAddress] -> + MeetingsSubsystem m Bool makeSem ''MeetingsSubsystem diff --git a/libs/wire-subsystems/src/Wire/MeetingsSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/MeetingsSubsystem/Interpreter.hs index e26c4719c64..45d06de2cae 100644 --- a/libs/wire-subsystems/src/Wire/MeetingsSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/MeetingsSubsystem/Interpreter.hs @@ -25,17 +25,20 @@ import Control.Monad.Trans.Maybe (MaybeT (MaybeT, runMaybeT)) import Data.Default (def) import Data.Domain (Domain) import Data.Id +import Data.Map qualified as Map import Data.Qualified (Local, Qualified (..), tDomain, tUnqualified) +import Data.Range (Range, unsafeRange) import Data.Set qualified as Set -import Data.Time.Clock (NominalDiffTime, addUTCTime) +import Data.Time.Clock (NominalDiffTime, UTCTime, addUTCTime) import Imports import Polysemy import Polysemy.Error import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.Role (roleNameWireAdmin) import Wire.API.Meeting qualified as API +import Wire.API.Routes.MultiTablePaging qualified as MultiTablePaging import Wire.API.Team.Feature (FeatureStatus (..), LockableFeature (..), MeetingsPremiumConfig) -import Wire.API.User (BaseProtocolTag (BaseProtocolMLSTag)) +import Wire.API.User (BaseProtocolTag (BaseProtocolMLSTag), EmailAddress) import Wire.ConversationSubsystem (ConversationSubsystem) import Wire.ConversationSubsystem qualified as ConversationSubsystem import Wire.FeaturesConfigSubsystem (FeaturesConfigSubsystem, getFeatureForTeam) @@ -67,6 +70,12 @@ interpretMeetingsSubsystem validityPeriod = interpret $ \case updateMeetingImpl zUser meetingId update validityPeriod GetMeeting zUser meetingId -> getMeetingImpl zUser meetingId validityPeriod + ListMeetings zUser -> + listMeetingsImpl zUser validityPeriod + AddInvitedEmails zUser meetingId emails -> + addInvitedEmailsImpl zUser meetingId emails validityPeriod + RemoveInvitedEmails zUser meetingId emails -> + removeInvitedEmailsImpl zUser meetingId emails validityPeriod createMeetingImpl :: ( Member Store.MeetingsStore r, @@ -152,6 +161,7 @@ updateMeetingImpl zUser meetingId update validityPeriod = do now <- lift Now.get let cutoff = addUTCTime (negate validityPeriod) now guard $ meeting.endTime >= cutoff + guard $ qDomain meetingId == tDomain zUser when (fromMaybe meeting.startTime update.startTime >= fromMaybe meeting.endTime update.endTime) $ lift $ throw InvalidTimes @@ -183,6 +193,7 @@ getMeetingImpl zUser meetingId validityPeriod = do now <- lift Now.get let cutoff = addUTCTime (negate validityPeriod) now guard $ storedMeeting.endTime >= cutoff + guard $ qDomain meetingId == tDomain zUser -- Check authorization: user must be creator OR member of the associated conversation let isCreator = storedMeeting.creator == tUnqualified zUser if isCreator @@ -209,3 +220,112 @@ storedMeetingToMeeting domain sm = API.createdAt = sm.createdAt, API.updatedAt = sm.updatedAt } + +listMeetingsImpl :: + ( Member Store.MeetingsStore r, + Member ConversationSubsystem r, + Member Now r + ) => + Local UserId -> + NominalDiffTime -> + Sem r [API.Meeting] +listMeetingsImpl zUser validityPeriod = do + now <- Now.get + let cutoff = addUTCTime (negate validityPeriod) now + -- List all meetings created by the user + createdMeetings <- Store.listMeetingsByUser (tUnqualified zUser) cutoff + -- Loop over local conversations accessible by the user, then filter to only keep meetings. + memberMeetings <- getAllMemberMeetings zUser cutoff + -- Combine and deduplicate + let allMeetings = map (storedMeetingToMeeting (tDomain zUser)) createdMeetings <> memberMeetings + uniqueMeetings = Map.elems $ Map.fromList [(m.id, m) | m <- allMeetings] + pure uniqueMeetings + +getAllMemberMeetings :: + ( Member Store.MeetingsStore r, + Member ConversationSubsystem r + ) => + Local UserId -> + UTCTime -> + Sem r [API.Meeting] +getAllMemberMeetings zUser cutoff = do + -- We process conversations in pages + processPage Nothing + where + processPage :: + ( Member Store.MeetingsStore r, + Member ConversationSubsystem r + ) => + Maybe ConversationPagingState -> Sem r [API.Meeting] + processPage pagingState = do + let range = unsafeRange 1000 :: Range 1 1000 Int32 + page <- ConversationSubsystem.getConversationIds zUser range pagingState + case page of + MultiTablePaging.MultiTablePage uConvIds hasMore _ -> + if null uConvIds + then pure [] + else do + convs <- ConversationSubsystem.getConversations (map qUnqualified uConvIds) + let meetingConvs = filter isMeetingConv convs + meetingConvIds = Set.fromList $ map (.id_) meetingConvs + -- Identify which Qualified ConvIds correspond to meeting conversations + -- We use the original Qualified IDs to query the meeting store + let targetQConvIds = filter (\qId -> qUnqualified qId `Set.member` meetingConvIds) uConvIds + -- Fetch meetings for these conversations + pageMeetings <- forM targetQConvIds $ \qConvId -> do + Store.listMeetingsByConversation (qUnqualified qConvId) cutoff + let currentMeetings = storedMeetingToMeeting (tDomain zUser) <$> concat pageMeetings + -- Check if there are more pages + if hasMore + then do + -- Recurse with paging state from the page + let nextPageState = Just page.mtpPagingState + rest <- processPage nextPageState + pure (currentMeetings <> rest) + else pure currentMeetings + isMeetingConv :: StoredConversation -> Bool + isMeetingConv conv = conv.metadata.cnvmGroupConvType == Just MeetingConversation + +addInvitedEmailsImpl :: + ( Member Store.MeetingsStore r, + Member Now r + ) => + Local UserId -> + Qualified MeetingId -> + [EmailAddress] -> + NominalDiffTime -> + Sem r Bool +addInvitedEmailsImpl zUser meetingId emails validityPeriod = do + result <- + runMaybeT $ do + storedMeeting <- MaybeT $ Store.getMeeting (qUnqualified meetingId) + now <- lift Now.get + let cutoff = addUTCTime (negate validityPeriod) now + guard $ storedMeeting.endTime >= cutoff + guard $ storedMeeting.creator == tUnqualified zUser + guard $ qDomain meetingId == tDomain zUser + lift $ Store.addInvitedEmails (qUnqualified meetingId) emails + + pure $ isJust result + +removeInvitedEmailsImpl :: + ( Member Store.MeetingsStore r, + Member Now r + ) => + Local UserId -> + Qualified MeetingId -> + [EmailAddress] -> + NominalDiffTime -> + Sem r Bool +removeInvitedEmailsImpl zUser meetingId emails validityPeriod = do + result <- + runMaybeT $ do + storedMeeting <- MaybeT $ Store.getMeeting (qUnqualified meetingId) + now <- lift Now.get + let cutoff = addUTCTime (negate validityPeriod) now + guard $ storedMeeting.endTime >= cutoff + guard $ storedMeeting.creator == tUnqualified zUser + guard $ qDomain meetingId == tDomain zUser + lift $ Store.removeInvitedEmails (qUnqualified meetingId) emails + + pure $ isJust result diff --git a/libs/wire-subsystems/src/Wire/NotificationSubsystem.hs b/libs/wire-subsystems/src/Wire/NotificationSubsystem.hs index 750d90885a9..9084d564986 100644 --- a/libs/wire-subsystems/src/Wire/NotificationSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/NotificationSubsystem.hs @@ -24,16 +24,24 @@ import Control.Lens (view) import Data.Aeson import Data.Default import Data.Id +import Data.Json.Util +import Data.List.NonEmpty qualified as NonEmpty +import Data.Map qualified as Map +import Data.Qualified import Imports import Polysemy +import Polysemy.TinyLog +import System.Logger.Class qualified as Log import Wire.API.Event.Conversation import Wire.API.Federation.API.Galley.Notifications (ConversationUpdate) +import Wire.API.Message 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 (..)) +import Wire.ExternalAccess +import Wire.StoredConversation (BotMember, LocalMember (..)) data Recipient = Recipient { recipientUserId :: UserId, @@ -107,3 +115,63 @@ instance Default Push where newPushLocal :: UserId -> Push newPushLocal uid = def {origin = Just uid} + +-- * Message-related + +data MessagePush + = MessagePush (Maybe ConnId) MessageMetadata [Recipient] [BotMember] Event + +type BotMap = Map UserId BotMember + +class ToRecipient a where + toRecipient :: a -> Recipient + +instance ToRecipient (UserId, ClientId) where + toRecipient (u, c) = Recipient u (RecipientClientsSome (NonEmpty.singleton c)) + +instance ToRecipient Recipient where + toRecipient = id + +newMessagePush :: + (ToRecipient r) => + BotMap -> + Maybe ConnId -> + MessageMetadata -> + [r] -> + Event -> + MessagePush +newMessagePush botMap mconn mm userOrBots event = + let toPair r = case Map.lookup (recipientUserId r) botMap of + Just botMember -> ([], [botMember]) + Nothing -> ([r], []) + (recipients, botMembers) = foldMap (toPair . toRecipient) userOrBots + in MessagePush mconn mm recipients botMembers event + +runMessagePush :: + forall x r. + ( Member ExternalAccess r, + Member TinyLog r, + Member NotificationSubsystem r + ) => + Local x -> + Maybe (Qualified ConvId) -> + MessagePush -> + Sem r () +runMessagePush loc mqcnv mp@(MessagePush _ _ _ botMembers event) = do + pushNotifications [toPush mp] + for_ mqcnv $ \qcnv -> + if tDomain loc /= qDomain qcnv + then unless (null botMembers) $ do + warn $ Log.msg ("Ignoring messages for local bots in a remote conversation" :: ByteString) . Log.field "conversation" (show qcnv) + else deliverAndDeleteAsync (qUnqualified qcnv) (map (,event) botMembers) + +toPush :: MessagePush -> Push +toPush (MessagePush mconn mm rs _ event) = + def + { origin = Just (qUnqualified (eventFromUserId (evtFrom event))), + conn = mconn, + json = toJSONObject event, + recipients = rs, + route = bool RouteDirect RouteAny (mmNativePush mm), + transient = mmTransient mm + } diff --git a/libs/wire-subsystems/src/Wire/NotificationSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/NotificationSubsystem/Interpreter.hs index f313b7d95d8..c93258449c8 100644 --- a/libs/wire-subsystems/src/Wire/NotificationSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/NotificationSubsystem/Interpreter.hs @@ -143,9 +143,9 @@ toV2Push p = pload :: NonEmpty Object pload = NonEmpty.singleton p.json recipients :: [V2.Recipient] - recipients = map toRecipient $ toList p.recipients - toRecipient :: Recipient -> V2.Recipient - toRecipient r = + recipients = map toV2Recipient $ toList p.recipients + toV2Recipient :: Recipient -> V2.Recipient + toV2Recipient r = (recipient r.recipientUserId p.route) { V2._recipientClients = r.recipientClients } diff --git a/libs/wire-subsystems/src/Wire/PasswordStore.hs b/libs/wire-subsystems/src/Wire/PasswordStore.hs index 0d01e4e7d43..d7598aeddf8 100644 --- a/libs/wire-subsystems/src/Wire/PasswordStore.hs +++ b/libs/wire-subsystems/src/Wire/PasswordStore.hs @@ -25,8 +25,7 @@ import Polysemy import Wire.API.Password data PasswordStore m a where - UpsertHashedPassword :: UserId -> Password -> PasswordStore m () - LookupHashedPassword :: UserId -> PasswordStore m (Maybe Password) + -- | FUTUREWORK: When we create ProviderStore, we should migrate this action to that. LookupHashedProviderPassword :: ProviderId -> PasswordStore m (Maybe Password) makeSem ''PasswordStore diff --git a/libs/wire-subsystems/src/Wire/PasswordStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/PasswordStore/Cassandra.hs index 576ca6cceec..ec36c348b45 100644 --- a/libs/wire-subsystems/src/Wire/PasswordStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/PasswordStore/Cassandra.hs @@ -31,8 +31,6 @@ interpretPasswordStore :: (Member (Embed IO) r) => ClientState -> InterpreterFor interpretPasswordStore casClient = interpret $ runEmbedded (runClient casClient) . \case - UpsertHashedPassword uid password -> embed $ updatePasswordImpl uid password - LookupHashedPassword uid -> embed $ lookupPasswordImpl uid LookupHashedProviderPassword pid -> embed $ lookupProviderPasswordImpl pid lookupProviderPasswordImpl :: (MonadClient m) => ProviderId -> m (Maybe Password) @@ -40,24 +38,9 @@ lookupProviderPasswordImpl u = (runIdentity =<<) <$> retry x1 (query1 providerPasswordSelect (params LocalQuorum (Identity u))) -lookupPasswordImpl :: (MonadClient m) => UserId -> m (Maybe Password) -lookupPasswordImpl u = - (runIdentity =<<) - <$> retry x1 (query1 passwordSelect (params LocalQuorum (Identity u))) - -updatePasswordImpl :: (MonadClient m) => UserId -> Password -> m () -updatePasswordImpl u p = do - retry x5 $ write userPasswordUpdate (params LocalQuorum (p, u)) - ------------------------------------------------------------------------ -- Queries providerPasswordSelect :: PrepQuery R (Identity ProviderId) (Identity (Maybe Password)) providerPasswordSelect = "SELECT password FROM provider WHERE id = ?" - -passwordSelect :: PrepQuery R (Identity UserId) (Identity (Maybe Password)) -passwordSelect = "SELECT password FROM user WHERE id = ?" - -userPasswordUpdate :: PrepQuery W (Password, UserId) () -userPasswordUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET password = ? WHERE id = ?" diff --git a/libs/wire-subsystems/src/Wire/PostgresMigrationOpts.hs b/libs/wire-subsystems/src/Wire/PostgresMigrationOpts.hs index 86fa90c878b..df635d14530 100644 --- a/libs/wire-subsystems/src/Wire/PostgresMigrationOpts.hs +++ b/libs/wire-subsystems/src/Wire/PostgresMigrationOpts.hs @@ -1,5 +1,3 @@ -{-# OPTIONS_GHC -fforce-recomp #-} - -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2025 Wire Swiss GmbH diff --git a/libs/wire-subsystems/src/Wire/StoredUser.hs b/libs/wire-subsystems/src/Wire/StoredUser.hs index 78e3cbc42c0..eec62c97a97 100644 --- a/libs/wire-subsystems/src/Wire/StoredUser.hs +++ b/libs/wire-subsystems/src/Wire/StoredUser.hs @@ -101,7 +101,7 @@ mkUserFromStored domain defaultLocale storedUser = svc = newServiceRef <$> storedUser.serviceId <*> storedUser.providerId in User { userQualifiedId = (Qualified storedUser.id domain), - userType = inferUserType (isJust svc) storedUser.userType, + userType = inferUserType svc storedUser.userType, userIdentity = storedUser.identity, userEmailUnvalidated = storedUser.emailUnvalidated, userDisplayName = storedUser.name, @@ -127,10 +127,13 @@ mkUserFromStored domain defaultLocale storedUser = -- The type is inferred as "bot" if there is a serviceId, and -- "regular" otherwise. For newly created apps, the second argument -- will always be `Just`. -inferUserType :: Bool {- is service -} -> Maybe UserType -> UserType -inferUserType True _ = UserTypeBot -inferUserType False Nothing = UserTypeRegular -inferUserType False (Just t) = t +-- +-- NB: The polymorphism is necessary because different caller have +-- different types of service ids in the `Maybe`. +inferUserType :: forall serviceId. Maybe serviceId -> Maybe UserType -> UserType +inferUserType _ (Just t) = t +inferUserType (Just _) Nothing = UserTypeBot +inferUserType Nothing Nothing = UserTypeRegular toLocale :: Locale -> (Maybe Language, Maybe Country) -> Locale toLocale _ (Just l, c) = Locale l c @@ -231,6 +234,33 @@ deriving instance instance HasField "service" NewStoredUser (Maybe ServiceRef) where getField user = ServiceRef <$> user.serviceId <*> user.providerId +newStoredUserToStoredUser :: NewStoredUser -> StoredUser +newStoredUserToStoredUser new = + StoredUser + { id = new.id, + userType = Just new.userType, + name = new.name, + textStatus = new.textStatus, + pict = Just new.pict, + email = new.email, + emailUnvalidated = new.email, + ssoId = new.ssoId, + accentId = new.accentId, + assets = Just new.assets, + activated = new.activated, + status = Just new.status, + expires = new.expires, + language = Just new.language, + country = new.country, + providerId = new.providerId, + serviceId = new.serviceId, + handle = new.handle, + teamId = new.teamId, + managedBy = Just new.managedBy, + supportedProtocols = Just new.supportedProtocols, + searchable = Just new.searchable + } + -- This saves the identity from `NewStoredUser` even if the user is -- not activated. newStoredUserToUser :: Qualified NewStoredUser -> User diff --git a/services/galley/src/Galley/Effects/TeamMemberStore.hs b/libs/wire-subsystems/src/Wire/TeamMemberStore.hs similarity index 85% rename from services/galley/src/Galley/Effects/TeamMemberStore.hs rename to libs/wire-subsystems/src/Wire/TeamMemberStore.hs index 84f2dbca287..0e056a2b0b1 100644 --- a/services/galley/src/Galley/Effects/TeamMemberStore.hs +++ b/libs/wire-subsystems/src/Wire/TeamMemberStore.hs @@ -2,7 +2,7 @@ -- This file is part of the Wire Server implementation. -- --- Copyright (C) 2022 Wire Swiss GmbH +-- Copyright (C) 2026 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,11 +17,8 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Galley.Effects.TeamMemberStore - ( -- * Team member store effect - TeamMemberStore (..), - - -- * Team member pagination +module Wire.TeamMemberStore + ( TeamMemberStore (..), listTeamMembers, ) where diff --git a/services/galley/src/Galley/Cassandra/Team.hs b/libs/wire-subsystems/src/Wire/TeamMemberStore/Cassandra.hs similarity index 73% rename from services/galley/src/Galley/Cassandra/Team.hs rename to libs/wire-subsystems/src/Wire/TeamMemberStore/Cassandra.hs index cb961573737..6978d80a6e3 100644 --- a/services/galley/src/Galley/Cassandra/Team.hs +++ b/libs/wire-subsystems/src/Wire/TeamMemberStore/Cassandra.hs @@ -1,6 +1,6 @@ -- This file is part of the Wire Server implementation. -- --- Copyright (C) 2022 Wire Swiss GmbH +-- Copyright (C) 2026 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,10 +15,8 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Galley.Cassandra.Team +module Wire.TeamMemberStore.Cassandra ( interpretTeamMemberStoreToCassandra, - interpretTeamListToCassandra, - interpretInternalTeamListToCassandra, interpretTeamMemberStoreToCassandraWithPaging, ) where @@ -32,9 +30,6 @@ import Data.Id import Data.Json.Util (UTCTimeMillis (..)) import Data.LegalHold (UserLegalHoldStatus (..), defUserLegalHoldStatus) import Data.Range -import Galley.Cassandra.Store -import Galley.Cassandra.Util -import Galley.Effects.TeamMemberStore import Imports hiding (Set, max) import Polysemy import Polysemy.Input @@ -43,37 +38,10 @@ import Wire.API.Team.Feature import Wire.API.Team.FeatureFlags (FeatureDefaults (..)) import Wire.API.Team.Member import Wire.API.Team.Permission (Permissions) -import Wire.ListItems import Wire.Sem.Paging.Cassandra +import Wire.TeamMemberStore import Wire.TeamStore.Cassandra.Queries qualified as Cql - -interpretTeamListToCassandra :: - ( Member (Embed IO) r, - Member (Input ClientState) r, - Member TinyLog r - ) => - Sem (ListItems LegacyPaging TeamId ': r) a -> - Sem r a -interpretTeamListToCassandra = interpret $ \case - ListItems uid ps lim -> do - logEffect "TeamList.ListItems" - embedClient $ teamIdsFrom uid ps lim - -interpretInternalTeamListToCassandra :: - ( Member (Embed IO) r, - Member (Input ClientState) r, - Member TinyLog r - ) => - Sem (ListItems InternalPaging TeamId ': r) a -> - Sem r a -interpretInternalTeamListToCassandra = interpret $ \case - ListItems uid mps lim -> do - logEffect "InternalTeamList.ListItems" - embedClient $ case mps of - Nothing -> do - page <- teamIdsForPagination uid Nothing lim - mkInternalPage page pure - Just ps -> ipNext ps +import Wire.Util (embedClientInput, logEffect) interpretTeamMemberStoreToCassandra :: ( Member (Embed IO) r, @@ -86,7 +54,7 @@ interpretTeamMemberStoreToCassandra :: interpretTeamMemberStoreToCassandra lh = interpret $ \case ListTeamMembers tid mps lim -> do logEffect "TeamMemberStore.ListTeamMembers" - embedClient $ case mps of + embedClientInput $ case mps of Nothing -> do page <- teamMembersForPagination tid Nothing lim mkInternalPage page (newTeamMember' lh tid) @@ -103,21 +71,7 @@ interpretTeamMemberStoreToCassandraWithPaging :: interpretTeamMemberStoreToCassandraWithPaging lh = interpret $ \case ListTeamMembers tid mps lim -> do logEffect "TeamMemberStore.ListTeamMembers" - embedClient $ teamMembersPageFrom lh tid mps lim - -teamIdsFrom :: UserId -> Maybe TeamId -> Range 1 100 Int32 -> Client (ResultSet TeamId) -teamIdsFrom usr range (fromRange -> max) = - mkResultSet . fmap runIdentity . strip <$> case range of - Just c -> paginate Cql.selectUserTeamsFrom (paramsP LocalQuorum (usr, c) (max + 1)) - Nothing -> paginate Cql.selectUserTeams (paramsP LocalQuorum (Identity usr) (max + 1)) - where - strip p = p {result = take (fromIntegral max) (result p)} - -teamIdsForPagination :: UserId -> Maybe TeamId -> Range 1 100 Int32 -> Client (Page TeamId) -teamIdsForPagination usr range (fromRange -> max) = - fmap runIdentity <$> case range of - Just c -> paginate Cql.selectUserTeamsFrom (paramsP LocalQuorum (usr, c) max) - Nothing -> paginate Cql.selectUserTeams (paramsP LocalQuorum (Identity usr) max) + embedClientInput $ teamMembersPageFrom lh tid mps lim -- | Construct 'TeamMember' from database tuple. -- If FeatureLegalHoldWhitelistTeamsAndImplicitConsent is enabled set UserLegalHoldDisabled diff --git a/services/galley/src/Galley/Effects/TeamNotificationStore.hs b/libs/wire-subsystems/src/Wire/TeamNotificationStore.hs similarity index 71% rename from services/galley/src/Galley/Effects/TeamNotificationStore.hs rename to libs/wire-subsystems/src/Wire/TeamNotificationStore.hs index e2aee046a34..3e5f50d567b 100644 --- a/services/galley/src/Galley/Effects/TeamNotificationStore.hs +++ b/libs/wire-subsystems/src/Wire/TeamNotificationStore.hs @@ -1,8 +1,9 @@ +{-# LANGUAGE StrictData #-} {-# LANGUAGE TemplateHaskell #-} -- This file is part of the Wire Server implementation. -- --- Copyright (C) 2022 Wire Swiss GmbH +-- Copyright (C) 2026 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,8 +18,15 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Galley.Effects.TeamNotificationStore - ( TeamNotificationStore (..), +-- | See also: "Galley.API.TeamNotifications". +-- +-- This module is a clone of "Gundeck.Notification.Data". +-- +-- FUTUREWORK: this is a work-around because it only solves *some* problems with team events. +-- We should really use a scalable message queue instead. +module Wire.TeamNotificationStore + ( ResultPage (..), + TeamNotificationStore (..), createTeamNotification, getTeamNotifications, mkNotificationId, @@ -29,11 +37,16 @@ import Data.Aeson qualified as JSON import Data.Id import Data.List.NonEmpty import Data.Range -import Galley.Data.TeamNotifications +import Data.Sequence (Seq) import Imports import Polysemy import Wire.API.Internal.Notification +data ResultPage = ResultPage + { resultSeq :: Seq QueuedNotification, + resultHasMore :: !Bool + } + data TeamNotificationStore m a where CreateTeamNotification :: TeamId -> diff --git a/services/galley/src/Galley/Cassandra/TeamNotifications.hs b/libs/wire-subsystems/src/Wire/TeamNotificationStore/Cassandra.hs similarity index 83% rename from services/galley/src/Galley/Cassandra/TeamNotifications.hs rename to libs/wire-subsystems/src/Wire/TeamNotificationStore/Cassandra.hs index 8558945a196..e520c0081ea 100644 --- a/services/galley/src/Galley/Cassandra/TeamNotifications.hs +++ b/libs/wire-subsystems/src/Wire/TeamNotificationStore/Cassandra.hs @@ -1,6 +1,6 @@ -- This file is part of the Wire Server implementation. -- --- Copyright (C) 2022 Wire Swiss GmbH +-- Copyright (C) 2026 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,13 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . --- | See also: "Galley.API.TeamNotifications". --- --- This module is a clone of "Gundeck.Notification.Data". --- --- FUTUREWORK: this is a work-around because it only solves *some* problems with team events. --- We should really use a scalable message queue instead. -module Galley.Cassandra.TeamNotifications +module Wire.TeamNotificationStore.Cassandra ( interpretTeamNotificationStoreToCassandra, ) where @@ -37,11 +31,6 @@ import Data.Sequence (Seq, ViewL (..), ViewR (..), (<|), (><)) import Data.Sequence qualified as Seq import Data.Time (nominalDay, nominalDiffTimeToSeconds) import Data.UUID.V1 qualified as UUID -import Galley.Cassandra.Store -import Galley.Cassandra.Util -import Galley.Data.TeamNotifications -import Galley.Effects -import Galley.Effects.TeamNotificationStore (TeamNotificationStore (..)) import Imports import Network.HTTP.Types import Network.Wai.Utilities hiding (Error) @@ -49,6 +38,8 @@ import Polysemy import Polysemy.Input import Polysemy.TinyLog hiding (err) import Wire.API.Internal.Notification +import Wire.TeamNotificationStore (ResultPage (..), TeamNotificationStore (..)) +import Wire.Util (embedClientInput, logEffect) interpretTeamNotificationStoreToCassandra :: ( Member (Embed IO) r, @@ -60,15 +51,14 @@ interpretTeamNotificationStoreToCassandra :: interpretTeamNotificationStoreToCassandra = interpret $ \case CreateTeamNotification tid nid objs -> do logEffect "TeamNotificationStore.CreateTeamNotification" - embedClient $ add tid nid objs + embedClientInput $ add tid nid objs GetTeamNotifications tid mnid lim -> do logEffect "TeamNotificationStore.GetTeamNotifications" - embedClient $ fetch tid mnid lim + embedClientInput $ fetch tid mnid lim MkNotificationId -> do logEffect "TeamNotificationStore.MkNotificationId" embed mkNotificationId --- | 'Data.UUID.V1.nextUUID' is sometimes unsuccessful, so we try a few times. mkNotificationId :: IO NotificationId mkNotificationId = do ni <- fmap Id <$> retrying x10 fun (const (liftIO UUID.nextUUID)) @@ -78,7 +68,6 @@ mkNotificationId = do fun = const (pure . isNothing) err = mkError status500 "internal-error" "unable to generate notification ID" --- FUTUREWORK: the magic 32 should be made configurable, so it can be tuned add :: TeamId -> NotificationId -> @@ -95,7 +84,6 @@ add tid nid (Blob . JSON.encode -> payload) = \USING TTL ?" -- | --- -- >>> import Data.Time -- >>> formatTime defaultTimeLocale "%d days, %H hours, %M minutes, %S seconds" (secondsToNominalDiffTime (fromIntegral notificationTTLSeconds)) -- "28 days, 0 hours, 0 minutes, 0 seconds" @@ -114,10 +102,10 @@ fetch tid since (fromRange -> size) = do -- or have found size + 1 notifications (not including the 'since'). let isize = fromIntegral size' :: Int (ns, more) <- collect Seq.empty isize page1 - -- Drop the extra element from the end as well. Keep the inclusive start + -- Drop the extra element from the end as well. Keep the inclusive start -- value in the response (if a 'since' was given and found). -- This can probably simplified a lot further, but we need to understand - -- 'Seq' in order to do that. If you find a bug, this may be a good + -- 'Seq' in order to do that. If you find a bug, this may be a good -- place to start looking. pure $! case Seq.viewl (trim (isize - 1) ns) of EmptyL -> ResultPage Seq.empty False @@ -137,18 +125,21 @@ fetch tid since (fromRange -> size) = do in if not more || num' == 0 then pure (acc', more || not (null (snd ns))) else liftClient (nextPage page) >>= collect acc' num' + trim :: Int -> Seq a -> Seq a trim l ns | Seq.length ns <= l = ns | otherwise = case Seq.viewr ns of EmptyR -> ns xs :> _ -> xs + cqlStart :: PrepQuery R (Identity TeamId) (TimeUuid, Blob) cqlStart = "SELECT id, payload \ \FROM team_notifications \ \WHERE team = ? \ \ORDER BY id ASC" + cqlSince :: PrepQuery R (TeamId, TimeUuid) (TimeUuid, Blob) cqlSince = "SELECT id, payload \ @@ -156,9 +147,6 @@ fetch tid since (fromRange -> size) = do \WHERE team = ? AND id >= ? \ \ORDER BY id ASC" -------------------------------------------------------------------------------- --- Conversions - toNotif :: (TimeUuid, Blob) -> [QueuedNotification] -> [QueuedNotification] toNotif (i, b) ns = maybe @@ -166,7 +154,7 @@ toNotif (i, b) ns = (\p1 -> queuedNotification notifId p1 : ns) ( JSON.decode' (fromBlob b) -- FUTUREWORK: this is from the database, so it's slightly more ok to ignore parse - -- errors than if it's data provided by a client. it would still be better to have an + -- errors than if it's data provided by a client. it would still be better to have an -- error entry in the log file and crash, rather than ignore the error and continue. ) where diff --git a/libs/wire-subsystems/src/Wire/TeamStore.hs b/libs/wire-subsystems/src/Wire/TeamStore.hs index bf45dd0cdf5..a86d27844e8 100644 --- a/libs/wire-subsystems/src/Wire/TeamStore.hs +++ b/libs/wire-subsystems/src/Wire/TeamStore.hs @@ -30,6 +30,7 @@ import Wire.API.Team import Wire.API.Team.Member (HardTruncationLimit, TeamMember, TeamMemberList) import Wire.API.Team.Member.Info (TeamMemberInfo) import Wire.API.Team.Permission +import Wire.API.Team.SearchVisibility (TeamSearchVisibility) import Wire.ListItems import Wire.Sem.Paging @@ -66,6 +67,9 @@ data TeamStore m a where DeleteTeam :: TeamId -> TeamStore m () SetTeamData :: TeamId -> TeamUpdateData -> TeamStore m () SetTeamStatus :: TeamId -> TeamStatus -> TeamStore m () + GetSearchVisibility :: TeamId -> TeamStore m TeamSearchVisibility + SetSearchVisibility :: TeamId -> TeamSearchVisibility -> TeamStore m () + ResetSearchVisibility :: TeamId -> TeamStore m () makeSem ''TeamStore diff --git a/libs/wire-subsystems/src/Wire/TeamStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/TeamStore/Cassandra.hs index 5a5f52468da..1fcf753c6a4 100644 --- a/libs/wire-subsystems/src/Wire/TeamStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/TeamStore/Cassandra.hs @@ -44,6 +44,7 @@ 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.API.Team.SearchVisibility import Wire.ConversationStore (ConversationStore) import Wire.ConversationStore qualified as E import Wire.ConversationStore.Cassandra.Instances () @@ -129,6 +130,15 @@ interpretTeamStoreToCassandra = interpret $ \case SetTeamStatus tid st -> do logEffect "TeamStore.SetTeamStatus" embedClientInput (updateTeamStatus tid st) + GetSearchVisibility tid -> do + logEffect "TeamStore.GetSearchVisibility" + embedClientInput $ getSearchVisibility tid + SetSearchVisibility tid value -> do + logEffect "TeamStore.SetSearchVisibility" + embedClientInput $ setSearchVisibility tid value + ResetSearchVisibility tid -> do + logEffect "TeamStore.ResetSearchVisibility" + embedClientInput $ resetSearchVisibility tid createTeam :: ( Member (Input ClientState) r, @@ -331,3 +341,23 @@ teamMemberInfos t u = mkTeamMemberInfo <$$> retry x1 (query Cql.selectTeamMember where mkTeamMemberInfo (uid, perms, permsWT, _, _, _) = TeamMemberInfo {Info.userId = uid, Info.permissions = perms, Info.permissionsWriteTime = toUTCTimeMillis $ writetimeToUTC permsWT} + +-- | Return whether a given team is allowed to enable/disable sso +getSearchVisibility :: (MonadClient m) => TeamId -> m TeamSearchVisibility +getSearchVisibility tid = + toSearchVisibility <$> do + retry x1 $ query1 Cql.selectSearchVisibility (params LocalQuorum (Identity tid)) + where + -- The value is either set or we return the default + toSearchVisibility :: Maybe (Identity (Maybe TeamSearchVisibility)) -> TeamSearchVisibility + toSearchVisibility (Just (Identity (Just status))) = status + toSearchVisibility _ = SearchVisibilityStandard + +-- | Determines whether a given team is allowed to enable/disable sso +setSearchVisibility :: (MonadClient m) => TeamId -> TeamSearchVisibility -> m () +setSearchVisibility tid visibilityType = do + retry x5 $ write Cql.updateSearchVisibility (params LocalQuorum (visibilityType, tid)) + +resetSearchVisibility :: (MonadClient m) => TeamId -> m () +resetSearchVisibility tid = do + retry x5 $ write Cql.updateSearchVisibility (params LocalQuorum (SearchVisibilityStandard, tid)) diff --git a/libs/wire-subsystems/src/Wire/TeamStore/Cassandra/Queries.hs b/libs/wire-subsystems/src/Wire/TeamStore/Cassandra/Queries.hs index 921c718030f..2167b3af0d0 100644 --- a/libs/wire-subsystems/src/Wire/TeamStore/Cassandra/Queries.hs +++ b/libs/wire-subsystems/src/Wire/TeamStore/Cassandra/Queries.hs @@ -27,6 +27,7 @@ import Text.RawString.QQ import Wire.API.Routes.Internal.Galley.TeamsIntra import Wire.API.Team import Wire.API.Team.Permission +import Wire.API.Team.SearchVisibility (TeamSearchVisibility) -- Teams -------------------------------------------------------------------- @@ -171,6 +172,14 @@ updateTeamStatus = "update team set status = ? where team = ?" updateTeamSplashScreen :: PrepQuery W (Text, TeamId) () updateTeamSplashScreen = "update team set splash_screen = ? where team = ?" +selectSearchVisibility :: PrepQuery R (Identity TeamId) (Identity (Maybe TeamSearchVisibility)) +selectSearchVisibility = + "select search_visibility from team where team = ?" + +updateSearchVisibility :: PrepQuery W (TeamSearchVisibility, TeamId) () +updateSearchVisibility = + {- `IF EXISTS`, but that requires benchmarking -} "update team set search_visibility = ? where team = ?" + -- LegalHold whitelist ------------------------------------------------------- selectLegalHoldWhitelistedTeam :: PrepQuery R (Identity TeamId) (Identity TeamId) diff --git a/libs/wire-subsystems/src/Wire/UserSearch/Types.hs b/libs/wire-subsystems/src/Wire/UserSearch/Types.hs index 28b766f0dba..5e8dcac765e 100644 --- a/libs/wire-subsystems/src/Wire/UserSearch/Types.hs +++ b/libs/wire-subsystems/src/Wire/UserSearch/Types.hs @@ -39,6 +39,7 @@ import Wire.API.Team.Role import Wire.API.User import Wire.API.User.Search import Wire.Arbitrary +import Wire.StoredUser newtype IndexVersion = IndexVersion {docVersion :: DocVersion} @@ -145,17 +146,19 @@ userDocToContact contactQualifiedId getName userDoc = contactHandle = fromHandle <$> userDoc.udHandle, contactTeam = userDoc.udTeam, contactType = - -- NB: after wire release upgrade and before ES reindexing, - -- apps may identify as regular users in the search result. - -- this is an accepted limitation and will be fixed in - -- https://github.com/wireapp/wire-server/pull/4947 - fromMaybe UserTypeRegular userDoc.udType + -- users of type `UserTypeBot` are not searchable as + -- contacts, so we can assume this is either + -- `UserTypeRegular` or `UserTypeApp`. + inferUserType Nothing userDoc.udType } userDocToTeamContact :: [UserGroupId] -> UserDoc -> TeamContact userDocToTeamContact userGroups UserDoc {..} = TeamContact { teamContactUserId = udId, + teamContactUserType = + -- bots are not searchable as contacts, so we can assume this is not one. + inferUserType Nothing udType, teamContactTeam = udTeam, teamContactSso = udSso, teamContactScimExternalId = udScimExternalId, diff --git a/libs/wire-subsystems/src/Wire/UserStore.hs b/libs/wire-subsystems/src/Wire/UserStore.hs index b8c27f82e40..5c35eeae407 100644 --- a/libs/wire-subsystems/src/Wire/UserStore.hs +++ b/libs/wire-subsystems/src/Wire/UserStore.hs @@ -105,6 +105,8 @@ data UserStore m a where GetRichInfo :: UserId -> UserStore m (Maybe RichInfoAssocList) LookupRichInfos :: [UserId] -> UserStore m [(UserId, RichInfo)] UpdateRichInfo :: UserId -> RichInfoAssocList -> UserStore m () + UpsertHashedPassword :: UserId -> Password -> UserStore m () + LookupHashedPassword :: UserId -> UserStore m (Maybe Password) GetUserAuthenticationInfo :: UserId -> UserStore m (Maybe (Maybe Password, AccountStatus)) SetUserSearchable :: UserId -> SetSearchable -> UserStore m () UpdateFeatureConferenceCalling :: UserId -> Maybe FeatureStatus -> UserStore m () diff --git a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs index 21d54bdc976..c9d4f54784b 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs @@ -71,6 +71,8 @@ interpretUserStoreCassandra casClient = UpdateUserTeam uid tid -> updateUserTeamImpl uid tid GetRichInfo uid -> getRichInfoImpl uid LookupRichInfos uids -> lookupRichInfosImpl uids + UpsertHashedPassword uid pw -> upsertHashedPasswordImpl uid pw + LookupHashedPassword uid -> lookupHashedPasswordImpl uid GetUserAuthenticationInfo uid -> getUserAuthenticationInfoImpl uid DeleteEmail uid -> deleteEmailImpl uid SetUserSearchable uid searchable -> setUserSearchableImpl uid searchable @@ -90,6 +92,21 @@ createUserImpl new mbConv = retry x5 . batch $ do for_ mbTid $ \tid -> addPrepQuery insertServiceTeam (pid, sid, BotId new.id, cid, tid) +upsertHashedPasswordImpl :: (MonadClient m) => UserId -> Password -> m () +upsertHashedPasswordImpl u p = do + retry x5 $ write userPasswordUpdate (params LocalQuorum (p, u)) + where + userPasswordUpdate :: PrepQuery W (Password, UserId) () + userPasswordUpdate = "UPDATE user SET password = ? WHERE id = ?" + +lookupHashedPasswordImpl :: (MonadClient m) => UserId -> m (Maybe Password) +lookupHashedPasswordImpl u = + (runIdentity =<<) + <$> retry x1 (query1 selectPassword (params LocalQuorum (Identity u))) + where + selectPassword :: PrepQuery R (Identity UserId) (Identity (Maybe Password)) + selectPassword = "SELECT password FROM user WHERE id = ?" + getUserAuthenticationInfoImpl :: UserId -> Client (Maybe (Maybe Password, AccountStatus)) getUserAuthenticationInfoImpl uid = fmap f <$> retry x1 (query1 authSelect (params LocalQuorum (Identity uid))) where @@ -132,6 +149,7 @@ getIndexUserBaseQuery = [sql| SELECT id, + user_type, team, writetime(team), name, writetime(name), status, writetime(status), diff --git a/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs b/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs index ce3d9221f2a..ddf6a1a6acb 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs @@ -45,6 +45,7 @@ data WithWritetime a = WithWriteTime {value :: a, writetime :: Writetime a} data IndexUser = IndexUser { userId :: UserId, + userType :: UserType, teamId :: Maybe TeamId, name :: Name, accountStatus :: Maybe AccountStatus, @@ -66,6 +67,7 @@ data IndexUser = IndexUser type instance TupleType IndexUser = ( UserId, + UserType, Maybe TeamId, Maybe (Writetime TeamId), Name, Writetime Name, Maybe AccountStatus, Maybe (Writetime AccountStatus), @@ -84,6 +86,7 @@ type instance indexUserFromTuple :: TupleType IndexUser -> IndexUser indexUserFromTuple ( userId, + userType, teamId, tTeam, name, tName, accountStatus, tStatus, @@ -121,13 +124,13 @@ indexUserToVersion :: Maybe (WithWritetime Role) -> IndexUser -> IndexVersion indexUserToVersion role iu = mkIndexVersion [Just $ Writetime iu.updatedAt, const () <$$> fmap writetime role] -indexUserToDoc :: SearchVisibilityInbound -> Maybe UserType -> Maybe Role -> IndexUser -> UserDoc -indexUserToDoc searchVisInbound mUserType mRole IndexUser {..} = +indexUserToDoc :: SearchVisibilityInbound -> Maybe Role -> IndexUser -> UserDoc +indexUserToDoc searchVisInbound mRole IndexUser {..} = if shouldIndex then UserDoc { udId = userId, - udType = mUserType, + udType = Just userType, udSearchable = searchable, udEmailUnvalidated = unverifiedEmail, udSso = sso =<< ssoId, diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem.hs b/libs/wire-subsystems/src/Wire/UserSubsystem.hs index 8df19a09311..8e20fdece8a 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem.hs @@ -118,6 +118,9 @@ data ChangeEmailResult ChangeEmailIdempotent deriving (Show) +data UserProfileFilter = Everything | RegularOnly | AppsOnly + deriving (Eq, Show) + data UserSubsystem m a where -- | First arg is for authorization only. GetUserProfiles :: Local UserId -> [Qualified UserId] -> UserSubsystem m [UserProfile] @@ -128,7 +131,7 @@ data UserSubsystem m a where -- FederationError)], [UserProfile])` to maintain API compatibility.) GetUserProfilesWithErrors :: Local UserId -> [Qualified UserId] -> UserSubsystem m ([(Qualified UserId, FederationError)], [UserProfile]) -- | Sometimes we don't have any identity of a requesting user, and local profiles are public. - GetLocalUserProfiles :: Local [UserId] -> UserSubsystem m [UserProfile] + GetLocalUserProfilesFiltered :: UserProfileFilter -> Local [UserId] -> UserSubsystem m [UserProfile] -- | Get the union of all user accounts matching the `GetBy` argument *and* having a non-empty UserIdentity. GetAccountsBy :: Local GetBy -> UserSubsystem m [User] -- | Get user accounts matching the `[EmailAddress]` argument (accounts with missing @@ -199,6 +202,22 @@ getLocalUserProfile :: (Member UserSubsystem r) => Local UserId -> Sem r (Maybe getLocalUserProfile targetUser = listToMaybe <$> getLocalUserProfiles ((: []) <$> targetUser) +getLocalUserProfileFiltered :: (Member UserSubsystem r) => UserProfileFilter -> Local UserId -> Sem r (Maybe UserProfile) +getLocalUserProfileFiltered upf targetUser = + listToMaybe <$> getLocalUserProfilesFiltered upf ((: []) <$> targetUser) + +getLocalUserProfileFiltered404 :: + (Member (Error UserSubsystemError) r, Member UserSubsystem r) => + UserProfileFilter -> Local UserId -> Sem r UserProfile +getLocalUserProfileFiltered404 upf targetUser = + getLocalUserProfileFiltered upf targetUser >>= note UserSubsystemProfileNotFound + +getLocalUserProfiles :: + (Member UserSubsystem r) => + Local [UserId] -> + Sem r [UserProfile] +getLocalUserProfiles = getLocalUserProfilesFiltered Everything + getLocalAccountBy :: (Member UserSubsystem r) => HavePendingInvitations -> diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs index 180689835ad..f3644d7af33 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs @@ -71,7 +71,7 @@ import Wire.API.User as User import Wire.API.User.RichInfo import Wire.API.User.Search import Wire.API.UserEvent -import Wire.AppStore +import Wire.AppSubsystem import Wire.AuthenticationSubsystem import Wire.BlockListStore as BlockList import Wire.ClientSubsystem (ClientSubsystem) @@ -108,7 +108,6 @@ import Witherable (wither) runUserSubsystem :: ( Member UserStore r, - Member AppStore r, Member UserKeyStore r, Member GalleyAPIAccess r, Member BlockListStore r, @@ -131,63 +130,65 @@ runUserSubsystem :: Member (Input UserSubsystemConfig) r, Member TeamSubsystem r, Member UserGroupStore r, - Member ClientSubsystem r + Member ClientSubsystem r, + Member (Input (Local any)) r ) => - InterpreterFor AuthenticationSubsystem r -> + InterpreterFor AuthenticationSubsystem (AppSubsystem ': r) -> + InterpreterFor AppSubsystem r -> Sem (UserSubsystem ': r) a -> Sem r a -runUserSubsystem authInterpreter = interpret $ - \case - GetUserProfiles self others -> - getUserProfilesImpl self others - GetLocalUserProfiles others -> - getLocalUserProfilesImpl others - GetAccountsBy getBy -> - getAccountsByImpl getBy - GetAccountsByEmailNoFilter emails -> - getAccountsByEmailNoFilterImpl emails - GetSelfProfile self -> - getSelfProfileImpl self - GetUserProfilesWithErrors self others -> - getUserProfilesWithErrorsImpl self others - UpdateUserProfile self mconn mb update -> - updateUserProfileImpl self mconn mb update - CheckHandle uhandle -> - checkHandleImpl uhandle - CheckHandles hdls cnt -> - checkHandlesImpl hdls cnt - UpdateHandle uid mconn mb uhandle -> - updateHandleImpl uid mconn mb uhandle - LookupLocaleWithDefault luid -> - lookupLocaleOrDefaultImpl luid - GuardRegisterActivateUserEmailDomain email -> - guardRegisterActivateUserEmailDomainImpl email - GuardUpgradePersonalUserToTeamEmailDomain email -> - guardUpgradePersonalUserToTeamEmailDomainImpl email - IsBlocked email -> - isBlockedImpl email - BlockListDelete email -> - blockListDeleteImpl email - BlockListInsert email -> - blockListInsertImpl email - UpdateTeamSearchVisibilityInbound status -> - updateTeamSearchVisibilityInboundImpl status - SearchUsers luid query mDomain mMaxResults mTypes -> - searchUsersImpl luid query mDomain mMaxResults mTypes - BrowseTeam uid browseTeamFilters mMaxResults mPagingState -> - browseTeamImpl uid browseTeamFilters mMaxResults mPagingState - InternalUpdateSearchIndex uid -> - syncUserIndex uid - AcceptTeamInvitation luid pwd code -> - authInterpreter $ +runUserSubsystem authInterpreter appInterpreter = + interpret $ + appInterpreter . authInterpreter . \case + GetUserProfiles self others -> + getUserProfilesImpl self others + GetLocalUserProfilesFiltered upf others -> + getLocalUserProfilesFilteredImpl upf others + GetAccountsBy getBy -> + getAccountsByImpl getBy + GetAccountsByEmailNoFilter emails -> + getAccountsByEmailNoFilterImpl emails + GetSelfProfile self -> + getSelfProfileImpl self + GetUserProfilesWithErrors self others -> + getUserProfilesWithErrorsImpl self others + UpdateUserProfile self mconn mb update -> + updateUserProfileImpl self mconn mb update + CheckHandle uhandle -> + checkHandleImpl uhandle + CheckHandles hdls cnt -> + checkHandlesImpl hdls cnt + UpdateHandle uid mconn mb uhandle -> + updateHandleImpl uid mconn mb uhandle + LookupLocaleWithDefault luid -> + lookupLocaleOrDefaultImpl luid + GuardRegisterActivateUserEmailDomain email -> + guardRegisterActivateUserEmailDomainImpl email + GuardUpgradePersonalUserToTeamEmailDomain email -> + guardUpgradePersonalUserToTeamEmailDomainImpl email + IsBlocked email -> + isBlockedImpl email + BlockListDelete email -> + blockListDeleteImpl email + BlockListInsert email -> + blockListInsertImpl email + UpdateTeamSearchVisibilityInbound status -> + updateTeamSearchVisibilityInboundImpl status + SearchUsers luid query mDomain mMaxResults mTypes -> + searchUsersImpl luid query mDomain mMaxResults mTypes + BrowseTeam uid browseTeamFilters mMaxResults mPagingState -> + browseTeamImpl uid browseTeamFilters mMaxResults mPagingState + InternalUpdateSearchIndex uid -> + syncUserIndex uid + AcceptTeamInvitation luid pwd code -> acceptTeamInvitationImpl luid pwd code - InternalFindTeamInvitation mEmailKey code -> - internalFindTeamInvitationImpl mEmailKey code - GetUserExportData uid -> getUserExportDataImpl uid - RemoveEmailEither luid -> removeEmailEitherImpl luid - UserSubsystem.GetUserTeam uid -> getUserTeamImpl uid - CheckUserIsAdmin uid -> checkUserIsAdminImpl uid - UserSubsystem.SetUserSearchable luid uid searchability -> setUserSearchableImpl luid uid searchability + InternalFindTeamInvitation mEmailKey code -> + internalFindTeamInvitationImpl mEmailKey code + GetUserExportData uid -> getUserExportDataImpl uid + RemoveEmailEither luid -> removeEmailEitherImpl luid + UserSubsystem.GetUserTeam uid -> getUserTeamImpl uid + CheckUserIsAdmin uid -> checkUserIsAdminImpl uid + UserSubsystem.SetUserSearchable luid uid searchability -> setUserSearchableImpl luid uid searchability scimExtId :: StoredUser -> Maybe Text scimExtId su = do @@ -312,7 +313,7 @@ blockListInsertImpl = BlockList.insert . mkEmailKey lookupLocaleOrDefaultImpl :: (Member UserStore r, Member (Input UserSubsystemConfig) r) => Local UserId -> Sem r (Maybe Locale) lookupLocaleOrDefaultImpl luid = do mLangCountry <- UserStore.lookupLocale (tUnqualified luid) - defLocale <- inputs defaultLocale + defLocale <- inputs (.defaultLocale) pure (toLocale defLocale <$> mLangCountry) -- | Obtain user profiles for a list of users as they can be seen by @@ -320,15 +321,16 @@ lookupLocaleOrDefaultImpl luid = do getUserProfilesImpl :: ( Member (Input UserSubsystemConfig) r, Member UserStore r, - Member AppStore r, Member (Concurrency 'Unsafe) r, -- FUTUREWORK: subsystems should implement concurrency inside interpreters, not depend on this dangerous effect. Member (Error FederationError) r, Member (FederationAPIAccess fedM) r, Member DeleteQueue r, Member Now r, + Member (Input (Local any)) r, RunClient (fedM 'Brig), FederationMonad fedM, Typeable fedM, + Member AppSubsystem r, Member TeamSubsystem r ) => -- | User 'self' on whose behalf the profiles are requested. @@ -343,19 +345,21 @@ getUserProfilesImpl self others = (getUserProfilesFromDomain self) (bucketQualified others) -getLocalUserProfilesImpl :: - forall r. +getLocalUserProfilesFilteredImpl :: + forall r any. ( Member UserStore r, - Member AppStore r, Member (Input UserSubsystemConfig) r, Member DeleteQueue r, Member Now r, Member (Concurrency Unsafe) r, - Member TeamSubsystem r + Member (Input (Local any)) r, + Member TeamSubsystem r, + Member AppSubsystem r ) => + UserProfileFilter -> Local [UserId] -> Sem r [UserProfile] -getLocalUserProfilesImpl = getUserProfilesLocalPart Nothing +getLocalUserProfilesFilteredImpl upf = getUserProfilesLocalPart upf Nothing getUserProfilesFromDomain :: ( Member (Error FederationError) r, @@ -364,12 +368,13 @@ getUserProfilesFromDomain :: Member DeleteQueue r, Member Now r, Member UserStore r, - Member AppStore r, RunClient (fedM 'Brig), FederationMonad fedM, Typeable fedM, + Member (Input (Local any)) r, Member (Concurrency Unsafe) r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member AppSubsystem r ) => Local UserId -> Qualified [UserId] -> @@ -377,7 +382,7 @@ getUserProfilesFromDomain :: getUserProfilesFromDomain self = foldQualified self - (getUserProfilesLocalPart (Just self)) + (getUserProfilesLocalPart Everything (Just self)) getUserProfilesRemotePart getUserProfilesRemotePart :: @@ -393,19 +398,21 @@ getUserProfilesRemotePart ruids = do runFederated ruids $ fedClient @'Brig @"get-users-by-ids" (tUnqualified ruids) getUserProfilesLocalPart :: - forall r. + forall r any. ( Member UserStore r, - Member AppStore r, Member (Input UserSubsystemConfig) r, Member DeleteQueue r, Member Now r, Member (Concurrency Unsafe) r, + Member (Input (Local any)) r, + Member AppSubsystem r, Member TeamSubsystem r ) => + UserProfileFilter -> Maybe (Local UserId) -> Local [UserId] -> Sem r [UserProfile] -getUserProfilesLocalPart requestingUser luids = do +getUserProfilesLocalPart upf requestingUser luids = do emailVisibilityConfig <- inputs emailVisibilityConfig requestingUserInfo <- join <$> traverse getRequestingUserInfo requestingUser let canSeeEmails = maybe False (isAdminOrOwner . view (newTeamMember . nPermissions) . snd) requestingUserInfo @@ -414,9 +421,10 @@ getUserProfilesLocalPart requestingUser luids = do EmailVisibleToSelf -> EmailVisibleToSelf EmailVisibleIfOnTeam -> EmailVisibleIfOnTeam EmailVisibleIfOnSameTeam () -> EmailVisibleIfOnSameTeam requestingUserInfo - -- FUTUREWORK: (in the interpreters where it makes sense) pull paginated lists from the DB, - -- not just single rows. - catMaybes <$> unsafePooledForConcurrentlyN 8 (sequence luids) (getLocalUserProfileImpl emailVisibilityConfigWithViewer) + injectAppsIntoUserProfiles . filter goUpf . catMaybes + -- FUTUREWORK: (in the interpreters where it makes sense) pull paginated lists from the DB, + -- not just single rows. + =<< unsafePooledForConcurrentlyN 8 (sequence luids) (getLocalUserProfileInternal emailVisibilityConfigWithViewer) where getRequestingUserInfo :: Local UserId -> Sem r (Maybe (TeamId, TeamMember)) getRequestingUserInfo self = do @@ -432,10 +440,15 @@ getUserProfilesLocalPart requestingUser luids = do Nothing -> pure Nothing Just tid -> (tid,) <$$> internalGetTeamMember (tUnqualified self) tid -getLocalUserProfileImpl :: + goUpf :: UserProfile -> Bool + goUpf prof = case upf of + Everything -> True + AppsOnly -> prof.profileType == UserTypeApp + RegularOnly -> prof.profileType == UserTypeRegular + +getLocalUserProfileInternal :: forall r. ( Member UserStore r, - Member AppStore r, Member DeleteQueue r, Member Now r, Member (Input UserSubsystemConfig) r, @@ -444,18 +457,17 @@ getLocalUserProfileImpl :: EmailVisibilityConfigWithViewer -> Local UserId -> Sem r (Maybe UserProfile) -getLocalUserProfileImpl emailVisibilityConfigWithViewer luid = do +getLocalUserProfileInternal emailVisibilityConfigWithViewer luid = do let domain = tDomain luid - locale <- inputs defaultLocale + locale <- inputs Wire.UserSubsystem.UserSubsystemConfig.defaultLocale runMaybeT $ do storedUser <- MaybeT $ getUser (tUnqualified luid) guard $ not (hasPendingInvitation storedUser) lhs :: UserLegalHoldStatus <- do teamMember <- lift $ join <$> (internalGetTeamMember storedUser.id `mapM` storedUser.teamId) pure $ maybe defUserLegalHoldStatus (view legalHoldStatus) teamMember - userType <- lift $ getUserType storedUser.id storedUser.teamId storedUser.serviceId let user = mkUserFromStored domain locale storedUser - usrProfile = mkUserProfile emailVisibilityConfigWithViewer userType user lhs + usrProfile = mkUserProfile emailVisibilityConfigWithViewer user Nothing lhs lift $ deleteLocalIfExpired user pure $ usrProfile @@ -467,7 +479,7 @@ getSelfProfileImpl :: Local UserId -> Sem r (Maybe SelfProfile) getSelfProfileImpl self = do - defLocale <- inputs defaultLocale + defLocale <- inputs Wire.UserSubsystem.UserSubsystemConfig.defaultLocale mStoredUser <- getUser (tUnqualified self) mHackedUser <- traverse hackForBlockingHandleChangeForE2EIdTeams mStoredUser let mUser = mkUserFromStored (tDomain self) defLocale <$> mHackedUser @@ -499,9 +511,8 @@ deleteLocalIfExpired user = enqueueUserDeletion (qUnqualified user.userQualifiedId) getUserProfilesWithErrorsImpl :: - forall r fedM. + forall r fedM any. ( Member UserStore r, - Member AppStore r, Member (Concurrency 'Unsafe) r, -- FUTUREWORK: subsystems should implement concurrency inside interpreters, not depend on this dangerous effect. Member (Input UserSubsystemConfig) r, Member (FederationAPIAccess fedM) r, @@ -510,13 +521,16 @@ getUserProfilesWithErrorsImpl :: RunClient (fedM 'Brig), FederationMonad fedM, Typeable fedM, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input (Local any)) r, + Member AppSubsystem r ) => Local UserId -> [Qualified UserId] -> Sem r ([(Qualified UserId, FederationError)], [UserProfile]) getUserProfilesWithErrorsImpl self others = do - aggregate ([], []) <$> unsafePooledMapConcurrentlyN 8 go (bucketQualified others) + (errs, profs_) <- aggregate ([], []) <$> unsafePooledMapConcurrentlyN 8 go (bucketQualified others) + (errs,) <$> injectAppsIntoUserProfiles profs_ where go :: Qualified [UserId] -> Sem r (Either (FederationError, Qualified [UserId]) [UserProfile]) go bucket = runError (getUserProfilesFromDomain self bucket) <&> mapLeft (,bucket) @@ -538,6 +552,28 @@ getUserProfilesWithErrorsImpl self others = do renderBucketError :: (FederationError, Qualified [UserId]) -> [(Qualified UserId, FederationError)] renderBucketError (e, qlist) = (,e) . (flip Qualified (qDomain qlist)) <$> qUnqualified qlist +-- | Consult `AppSubsystem` to get `AppInfo`s info for `UserProfile`'s +-- `app` attribute. +-- +-- NOTE ON PERFORMANCE: this function calls `getApp` once per user +-- profile. we could call `getApps` once instead, build a map from +-- that, and look up `AppInfo` for all profiles in memory. There is a +-- trade-off between memory usage and database IO, and we should +-- measure this before we make a change. +injectAppsIntoUserProfiles :: + (Member AppSubsystem r, Member (Input (Local x0)) r) => + [UserProfile] -> Sem r [UserProfile] +injectAppsIntoUserProfiles = mapM \uprof -> do + mbluid :: Maybe (Local UserId) <- do + localDom <- input + pure $ foldQualified localDom Just (const Nothing) uprof.profileQualifiedId + + mbApp :: Maybe AppInfo <- case (uprof.profileDeleted, uprof.profileType, mbluid, uprof.profileTeam) of + (False, UserTypeApp, Just luid, Just tid) -> Just <$> getApp luid tid (tUnqualified luid) + _ -> pure Nothing + + pure (uprof {profileApp = mbApp}) + -- | Some fields cannot be overwritten by clients for scim-managed users; some others if e2eid -- is used. If a client attempts to overwrite any of these, throw `UserSubsystem*ManagedByScim`. guardLockedFields :: @@ -578,7 +614,6 @@ guardLockedHandleField user updateOrigin handle = do updateUserProfileImpl :: ( Member UserStore r, - Member AppStore r, Member (Error UserSubsystemError) r, Member Events r, Member GalleyAPIAccess r, @@ -643,7 +678,6 @@ updateHandleImpl :: Member GalleyAPIAccess r, Member Events r, Member UserStore r, - Member AppStore r, Member IndexedUserStore r, Member Metrics r ) => @@ -710,7 +744,6 @@ checkHandlesImpl check num = reverse <$> collectFree [] check num syncUserIndex :: forall r. ( Member UserStore r, - Member AppStore r, Member GalleyAPIAccess r, Member IndexedUserStore r, Member Metrics r @@ -734,9 +767,8 @@ syncUserIndex uid = teamSearchVisibilityInbound indexUser.teamId tm <- maybe (pure Nothing) selectTeamMember indexUser.teamId - userType <- getUserType indexUser.userId indexUser.teamId indexUser.serviceId let mRole = tm >>= mkRoleWithWriteTime - userDoc = indexUserToDoc vis (Just userType) (value <$> mRole) indexUser + userDoc = indexUserToDoc vis (value <$> mRole) indexUser version = ES.ExternalGT . ES.ExternalDocVersion . docVersion $ indexUserToVersion mRole indexUser Metrics.incCounter indexUpdateCounter IndexedUserStore.upsert (userIdToDocId uid) userDoc version @@ -764,7 +796,6 @@ searchUsersImpl :: forall r fedM. ( Member UserStore r, Member GalleyAPIAccess r, - Member AppStore r, Member (Error UserSubsystemError) r, Member IndexedUserStore r, Member FederationConfigStore r, @@ -801,7 +832,6 @@ searchUsersImpl searcherId searchTerm maybeDomain maybeMaxResults mTypes = do searchLocally :: forall r. ( Member GalleyAPIAccess r, - Member AppStore r, Member UserStore r, Member IndexedUserStore r, Member (Input UserSubsystemConfig) r @@ -880,7 +910,6 @@ searchLocally searcher searchTerm maybeMaxResults mTypes = do || (not config.searchSameTeamOnly) if isContactVisible && fromMaybe True storedUser.searchable then do - userType <- lift $ getUserType storedUser.id storedUser.teamId storedUser.serviceId pure $ Contact { contactQualifiedId = Qualified storedUser.id (tDomain searcher), @@ -888,7 +917,7 @@ searchLocally searcher searchTerm maybeMaxResults mTypes = do contactHandle = Handle.fromHandle <$> storedUser.handle, contactColorId = Just . fromIntegral . fromColourId $ storedUser.accentId, contactTeam = storedUser.teamId, - contactType = userType + contactType = inferUserType storedUser.serviceId storedUser.userType } else hoistMaybe Nothing @@ -1054,7 +1083,6 @@ getAccountsByImpl (tSplit -> (domain, GetBy {..})) = do acceptTeamInvitationImpl :: ( Member (Input UserSubsystemConfig) r, Member UserStore r, - Member AppStore r, Member GalleyAPIAccess r, Member (Error UserSubsystemError) r, Member InvitationStore r, @@ -1119,7 +1147,6 @@ getUserExportDataImpl uid = fmap hush . runError @() $ do removeEmailEitherImpl :: ( Member UserKeyStore r, Member UserStore r, - Member AppStore r, Member Events r, Member IndexedUserStore r, Member (Input UserSubsystemConfig) r, @@ -1154,7 +1181,6 @@ checkUserIsAdminImpl uid = do setUserSearchableImpl :: ( Member UserStore r, - Member AppStore r, Member (Error UserSubsystemError) r, Member TeamSubsystem r, Member GalleyAPIAccess r, @@ -1170,20 +1196,3 @@ setUserSearchableImpl luid uid searchable = do ensurePermissions (tUnqualified luid) tid [SetMemberSearchable] UserStore.setUserSearchable uid searchable syncUserIndex uid - --- * Helpers - -getUserType :: - forall r. - (Member AppStore r) => - UserId -> - Maybe TeamId -> - Maybe ServiceId -> - Sem r UserType -getUserType uid mTid mbServiceId = case mbServiceId of - Just _ -> pure UserTypeBot - Nothing -> do - mmApp <- mapM (getApp uid) mTid - case join mmApp of - Just _ -> pure UserTypeApp - Nothing -> pure UserTypeRegular diff --git a/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs index df6c4ae3e7a..5c368928eea 100644 --- a/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs @@ -21,8 +21,10 @@ module Wire.AuthenticationSubsystem.InterpreterSpec (spec) where import Data.Domain import Data.Id +import Data.Map qualified as Map import Data.Misc import Data.Qualified +import Data.Range (rcast) import Data.Set qualified as Set import Data.Text.Encoding (decodeUtf8) import Data.Time @@ -38,9 +40,8 @@ import Test.Hspec import Test.Hspec.QuickCheck import Test.QuickCheck import Wire.API.Allowlists (AllowlistEmailDomains (AllowlistEmailDomains)) -import Wire.API.Password as Password +import Wire.API.Password import Wire.API.User -import Wire.API.User qualified as User import Wire.API.User.Auth import Wire.API.User.Password import Wire.API.UserEvent @@ -86,28 +87,28 @@ type AllEffects = TinyLog, EmailSubsystem, UserStore, + UserKeyStore, State [MiniEvent], - State [StoredUser], State (Map EmailAddress [SentMail]), State [StoredApp] ] -runAllEffects :: Domain -> [User] -> Maybe [Text] -> Sem AllEffects a -> Either AuthenticationSubsystemError a -runAllEffects domain users emailDomains action = snd $ runAllEffectsWithEventState domain users emailDomains action +runAllEffects :: Domain -> [StoredUser] -> Map UserId Password -> Maybe [Text] -> Sem AllEffects a -> Either AuthenticationSubsystemError a +runAllEffects domain users passwords emailDomains action = snd $ runAllEffectsWithEventState domain users passwords emailDomains action -runAllEffectsWithEventState :: Domain -> [User] -> Maybe [Text] -> Sem AllEffects a -> ([MiniEvent], Either AuthenticationSubsystemError a) -runAllEffectsWithEventState localDomain preexistingUsers mAllowedEmailDomains = +runAllEffectsWithEventState :: Domain -> [StoredUser] -> Map UserId Password -> Maybe [Text] -> Sem AllEffects a -> ([MiniEvent], Either AuthenticationSubsystemError a) +runAllEffectsWithEventState localDomain preexistingUsers preexistingPasswords mAllowedEmailDomains = let cfg = defaultAuthenticationSubsystemConfig { allowlistEmailDomains = AllowlistEmailDomains <$> mAllowedEmailDomains, local = toLocalUnsafe localDomain () } in run - . evalState mempty . evalState mempty . evalState mempty . runState mempty - . inMemoryUserStoreInterpreter + . runInMemoryUserKeyStoreIntepreterWithStoredUsers preexistingUsers + . runInMemoryUserStoreInterpreter preexistingUsers preexistingPasswords . inMemoryEmailSubsystemInterpreter . discardTinyLogs . evalState mempty @@ -125,14 +126,11 @@ runAllEffectsWithEventState localDomain preexistingUsers mAllowedEmailDomains = . runErrorUnsafe . runError . miniEventInterpreter - . interpretAuthenticationSubsystem (userSubsystemTestInterpreter preexistingUsers) + . interpretAuthenticationSubsystem inMemoryUserSubsystemInterpreter -verifyPasswordPure :: PlainTextPassword' t -> Password -> Bool -verifyPasswordPure plain hashed = - run - . noRateLimit - . staticHashPasswordInterpreter - $ verifyPassword (RateLimitIp (IpAddr "0.0.0.0")) plain hashed +toInputPassword :: PlainTextPassword8 -> PlainTextPassword6 +toInputPassword pw8 = + PlainTextPassword' . rcast $ fromPlainTextPassword' pw8 spec :: Spec spec = describe "AuthenticationSubsystem.Interpreter" do @@ -141,54 +139,57 @@ spec = describe "AuthenticationSubsystem.Interpreter" do \email userNoEmail (cookiesWithTTL :: [(Cookie (), Maybe TTL)]) mPreviousPassword newPassword -> let user = userNoEmail - { userIdentity = Just $ EmailIdentity email, - userEmailUnvalidated = Nothing, - userStatus = Active + { email = Just email, + emailUnvalidated = Nothing, + status = Just Active } - uid = User.userId user - localDomain = userNoEmail.userQualifiedId.qDomain - Right (newPasswordHash, cookiesAfterReset) = - runAllEffects localDomain [user] Nothing $ do - forM_ mPreviousPassword (hashPassword >=> upsertHashedPassword uid) + uid = user.id + passwords = foldMap (Map.singleton uid . hashPassword) mPreviousPassword + eithRes = + runAllEffects testDomain [user] passwords Nothing $ do mapM_ (uncurry (insertCookie uid)) cookiesWithTTL createPasswordResetCode (mkEmailKey email) (_, resetCode) <- expect1ResetPasswordEmail email resetPassword (PasswordResetEmailIdentity email) resetCode newPassword - - (,) <$> lookupHashedPassword uid <*> listCookies uid - in mPreviousPassword /= Just newPassword ==> - (fmap (verifyPasswordPure newPassword) newPasswordHash === Just True) - .&&. (cookiesAfterReset === []) + (,,) + <$> forM mPreviousPassword (verifyUserPassword uid . toInputPassword) + <*> verifyUserPassword uid (toInputPassword newPassword) + <*> listCookies uid + in case eithRes of + Left e -> counterexample ("Unexpected Error: " <> show e) False + Right (mOldPasswordVerification, newPasswordVerification, cookiesAfterReset) -> + (maybe (property True) (\(verification, _) -> verification === False) mOldPasswordVerification) + .&&. fst newPasswordVerification === True + .&&. (cookiesAfterReset === []) prop "password reset should work with the returned password reset key" $ \email userNoEmail (cookiesWithTTL :: [(Cookie (), Maybe TTL)]) mPreviousPassword newPassword -> let user = userNoEmail - { userIdentity = Just $ EmailIdentity email, - userEmailUnvalidated = Nothing, - userStatus = Active + { email = Just email, + emailUnvalidated = Nothing, + status = Just Active } - uid = User.userId user - localDomain = userNoEmail.userQualifiedId.qDomain - Right (newPasswordHash, cookiesAfterReset) = - runAllEffects localDomain [user] Nothing $ do - forM_ mPreviousPassword (hashPassword >=> upsertHashedPassword uid) + uid = user.id + passwords = foldMap (Map.singleton uid . hashPassword) mPreviousPassword + Right (newPasswordVerification, cookiesAfterReset) = + runAllEffects testDomain [user] passwords Nothing $ do mapM_ (uncurry (insertCookie uid)) cookiesWithTTL createPasswordResetCode (mkEmailKey email) (passwordResetKey, resetCode) <- expect1ResetPasswordEmail email resetPassword (PasswordResetIdentityKey passwordResetKey) resetCode newPassword - (,) <$> lookupHashedPassword uid <*> listCookies uid + (,) <$> verifyUserPassword uid (toInputPassword newPassword) <*> listCookies uid in mPreviousPassword /= Just newPassword ==> - (fmap (verifyPasswordPure newPassword) newPasswordHash === Just True) + (fst newPasswordVerification === True) .&&. (cookiesAfterReset === []) prop "reset code is not generated when email is not in allow list" $ \email localDomain -> let createPasswordResetCodeResult = - runAllEffects localDomain [] (Just ["example.com"]) $ + runAllEffects localDomain [] mempty (Just ["example.com"]) $ createPasswordResetCode (mkEmailKey email) <* expectNoEmailSent in domainPart email /= "example.com" ==> @@ -198,13 +199,12 @@ spec = describe "AuthenticationSubsystem.Interpreter" do \email userNoEmail -> let user = userNoEmail - { userIdentity = Just $ EmailIdentity email, - userEmailUnvalidated = Nothing, - userStatus = Active + { email = Just email, + emailUnvalidated = Nothing, + status = Just Active } - localDomain = userNoEmail.userQualifiedId.qDomain createPasswordResetCodeResult = - runAllEffects localDomain [user] (Just [decodeUtf8 $ domainPart email]) $ + runAllEffects testDomain [user] mempty (Just [decodeUtf8 $ domainPart email]) $ createPasswordResetCode (mkEmailKey email) in counterexample ("expected Right, got: " <> show createPasswordResetCodeResult) $ isRight createPasswordResetCodeResult @@ -213,21 +213,20 @@ spec = describe "AuthenticationSubsystem.Interpreter" do \email userNoEmail -> let user = userNoEmail - { userIdentity = Just $ EmailIdentity email, - userEmailUnvalidated = Nothing + { email = Just email, + emailUnvalidated = Nothing } - localDomain = userNoEmail.userQualifiedId.qDomain createPasswordResetCodeResult = - runAllEffects localDomain [user] Nothing $ + runAllEffects testDomain [user] mempty Nothing $ createPasswordResetCode (mkEmailKey email) <* expectNoEmailSent - in userStatus user /= Active ==> + in (isJust user.status && user.status /= Just Active) ==> createPasswordResetCodeResult === Right () prop "reset code is not generated for when there is no user for the email" $ \email localDomain -> let createPasswordResetCodeResult = - runAllEffects localDomain [] Nothing $ + runAllEffects localDomain [] mempty Nothing $ createPasswordResetCode (mkEmailKey email) <* expectNoEmailSent in createPasswordResetCodeResult === Right () @@ -236,14 +235,13 @@ spec = describe "AuthenticationSubsystem.Interpreter" do \email userNoEmail newPassword -> let user = userNoEmail - { userIdentity = Just $ EmailIdentity email, - userEmailUnvalidated = Nothing, - userStatus = Active + { email = Just email, + emailUnvalidated = Nothing, + status = Just Active } - uid = User.userId user - localDomain = userNoEmail.userQualifiedId.qDomain - Right (newPasswordHash, mCaughtException) = - runAllEffects localDomain [user] Nothing $ do + uid = user.id + Right (newPasswordVerification, mCaughtException) = + runAllEffects testDomain [user] mempty Nothing $ do createPasswordResetCode (mkEmailKey email) (_, resetCode) <- expect1ResetPasswordEmail email @@ -252,83 +250,88 @@ spec = describe "AuthenticationSubsystem.Interpreter" do -- Reset password still works with previously generated reset code resetPassword (PasswordResetEmailIdentity email) resetCode newPassword - (,mCaughtExc) <$> lookupHashedPassword uid - in (fmap (verifyPasswordPure newPassword) newPasswordHash === Just True) + (,mCaughtExc) <$> verifyUserPassword uid (toInputPassword newPassword) + in (fst newPasswordVerification === True) .&&. (mCaughtException === Nothing) prop "reset code is not accepted after expiry" $ \email userNoEmail oldPassword newPassword -> let user = userNoEmail - { userIdentity = Just $ EmailIdentity email, - userEmailUnvalidated = Nothing, - userStatus = Active + { email = Just email, + emailUnvalidated = Nothing, + status = Just Active } - uid = User.userId user - localDomain = userNoEmail.userQualifiedId.qDomain - Right (passwordInDB, resetPasswordResult) = - runAllEffects localDomain [user] Nothing $ do - upsertHashedPassword uid =<< hashPassword oldPassword + uid = user.id + passwords = Map.singleton uid $ hashPassword oldPassword + Right (oldPasswordVerification, newPasswordVerification, resetPasswordResult) = + runAllEffects testDomain [user] passwords Nothing $ do createPasswordResetCode (mkEmailKey email) (_, resetCode) <- expect1ResetPasswordEmail email passTime (passwordResetCodeTtl + 1) mCaughtExc <- catchExpectedError $ resetPassword (PasswordResetEmailIdentity email) resetCode newPassword - (,mCaughtExc) <$> lookupHashedPassword uid + (,,mCaughtExc) + <$> verifyUserPassword uid (toInputPassword oldPassword) + <*> verifyUserPassword uid (toInputPassword newPassword) in resetPasswordResult === Just AuthenticationSubsystemInvalidPasswordResetCode - .&&. verifyPasswordProp oldPassword passwordInDB + .&&. fst oldPasswordVerification === True + .&&. fst newPasswordVerification === False prop "password reset is not allowed with arbitrary codes when no other codes exist" $ \email userNoEmail resetCode oldPassword newPassword -> let user = userNoEmail - { userIdentity = Just $ EmailIdentity email, - userEmailUnvalidated = Nothing, - userStatus = Active + { email = Just email, + emailUnvalidated = Nothing, + status = Just Active } - uid = User.userId user - localDomain = userNoEmail.userQualifiedId.qDomain - Right (passwordInDB, resetPasswordResult) = - runAllEffects localDomain [user] Nothing $ do - upsertHashedPassword uid =<< hashPassword oldPassword + uid = user.id + passwords = Map.singleton uid $ hashPassword oldPassword + Right (oldPasswordVerification, newPasswordVerification, resetPasswordResult) = + runAllEffects testDomain [user] passwords Nothing $ do mCaughtExc <- catchExpectedError $ resetPassword (PasswordResetEmailIdentity email) resetCode newPassword - (,mCaughtExc) <$> lookupHashedPassword uid + (,,mCaughtExc) + <$> verifyUserPassword uid (toInputPassword oldPassword) + <*> verifyUserPassword uid (toInputPassword newPassword) in resetPasswordResult === Just AuthenticationSubsystemInvalidPasswordResetCode - .&&. verifyPasswordProp oldPassword passwordInDB + .&&. fst oldPasswordVerification === True + .&&. fst newPasswordVerification === False prop "password reset doesn't work if email is wrong" $ \email wrongEmail userNoEmail resetCode oldPassword newPassword -> let user = userNoEmail - { userIdentity = Just $ EmailIdentity email, - userEmailUnvalidated = Nothing, - userStatus = Active + { email = Just email, + emailUnvalidated = Nothing, + status = Just Active } - uid = User.userId user - localDomain = userNoEmail.userQualifiedId.qDomain - Right (passwordInDB, resetPasswordResult) = - runAllEffects localDomain [user] Nothing $ do - hashAndUpsertPassword uid oldPassword + uid = user.id + passwords = Map.singleton uid $ hashPassword oldPassword + Right (oldPasswordVerification, newPasswordVerification, resetPasswordResult) = + runAllEffects testDomain [user] passwords Nothing $ do mCaughtExc <- catchExpectedError $ resetPassword (PasswordResetEmailIdentity wrongEmail) resetCode newPassword - (,mCaughtExc) <$> lookupHashedPassword uid + (,,mCaughtExc) + <$> verifyUserPassword uid (toInputPassword oldPassword) + <*> verifyUserPassword uid (toInputPassword newPassword) in email /= wrongEmail ==> resetPasswordResult === Just AuthenticationSubsystemInvalidPasswordResetKey - .&&. verifyPasswordProp oldPassword passwordInDB + .&&. fst oldPasswordVerification === True + .&&. fst newPasswordVerification === False prop "only 3 wrong password reset attempts are allowed" $ \email userNoEmail arbitraryResetCode oldPassword newPassword (Upto4 wrongResetAttempts) -> let user = userNoEmail - { userIdentity = Just $ EmailIdentity email, - userEmailUnvalidated = Nothing, - userStatus = Active + { email = Just email, + emailUnvalidated = Nothing, + status = Just Active } - uid = User.userId user - localDomain = userNoEmail.userQualifiedId.qDomain - Right (passwordHashInDB, correctResetCode, wrongResetErrors, resetPassworedWithCorectCodeResult) = - runAllEffects localDomain [user] Nothing $ do - upsertHashedPassword uid =<< hashPassword oldPassword + uid = user.id + passwords = Map.singleton uid $ hashPassword oldPassword + Right (oldPasswordVerification, newPasswordVerification, correctResetCode, wrongResetErrors, resetPassworedWithCorectCodeResult) = + runAllEffects testDomain [user] passwords Nothing $ do createPasswordResetCode (mkEmailKey email) (_, generatedResetCode) <- expect1ResetPasswordEmail email @@ -338,42 +341,43 @@ spec = describe "AuthenticationSubsystem.Interpreter" do resetPassword (PasswordResetEmailIdentity email) arbitraryResetCode newPassword mFinalResetErr <- catchExpectedError $ resetPassword (PasswordResetEmailIdentity email) generatedResetCode newPassword - (,generatedResetCode,wrongResetErrs,mFinalResetErr) <$> lookupHashedPassword uid + (,,generatedResetCode,wrongResetErrs,mFinalResetErr) + <$> verifyUserPassword uid (toInputPassword oldPassword) + <*> verifyUserPassword uid (toInputPassword newPassword) expectedFinalResetResult = if wrongResetAttempts >= 3 then Just AuthenticationSubsystemInvalidPasswordResetCode else Nothing - expectedFinalPassword = + assertPasswordVerification = if wrongResetAttempts >= 3 - then oldPassword - else newPassword + then fst oldPasswordVerification === True .&&. fst newPasswordVerification === False + else fst oldPasswordVerification === False .&&. fst newPasswordVerification === True in correctResetCode /= arbitraryResetCode ==> wrongResetErrors == replicate wrongResetAttempts (Just AuthenticationSubsystemInvalidPasswordResetCode) .&&. resetPassworedWithCorectCodeResult === expectedFinalResetResult - .&&. verifyPasswordProp expectedFinalPassword passwordHashInDB + .&&. assertPasswordVerification describe "internalLookupPasswordResetCode" do prop "should find password reset code by email" $ \email userNoEmail newPassword -> let user = userNoEmail - { userIdentity = Just $ EmailIdentity email, - userEmailUnvalidated = Nothing, - userStatus = Active + { email = Just email, + emailUnvalidated = Nothing, + status = Just Active } - uid = User.userId user - localDomain = userNoEmail.userQualifiedId.qDomain - Right passwordHashInDB = - runAllEffects localDomain [user] Nothing $ do + uid = user.id + Right newPasswordVerification = + runAllEffects testDomain [user] mempty Nothing $ do void $ createPasswordResetCode (mkEmailKey email) mLookupRes <- internalLookupPasswordResetCode (mkEmailKey email) for_ mLookupRes $ \(_, resetCode) -> resetPassword (PasswordResetEmailIdentity email) resetCode newPassword - lookupHashedPassword uid - in verifyPasswordProp newPassword passwordHashInDB + verifyUserPassword uid (toInputPassword newPassword) + in fst newPasswordVerification === True describe "newCookie" $ do prop "trivial attributes: plain user cookie" $ \localDomain uid cid typ mLabel -> - let Right (plainCookie, lhCookie) = runAllEffects localDomain [] Nothing $ do + let Right (plainCookie, lhCookie) = runAllEffects localDomain [] mempty Nothing $ do plain <- newCookie @_ @ZAuth.U uid cid typ mLabel RevokeSameLabel lh <- newCookie @_ @ZAuth.U uid cid typ mLabel RevokeSameLabel pure (plain, lh) @@ -387,19 +391,19 @@ spec = describe "AuthenticationSubsystem.Interpreter" do prop "persistent plain cookie expires at configured time" $ \localDomain uid cid mLabel -> - let Right cookie = runAllEffects localDomain [] Nothing $ do + let Right cookie = runAllEffects localDomain [] mempty Nothing $ do newCookie @_ @ZAuth.U uid cid PersistentCookie mLabel RevokeSameLabel in cookie.cookieExpires === addUTCTime (fromIntegral defaultZAuthSettings.userTokenTimeout.userTokenTimeoutSeconds) defaultTime prop "persistent LH cookie expires at configured time" $ \localDomain uid cid mLabel -> - let Right cookie = runAllEffects localDomain [] Nothing $ do + let Right cookie = runAllEffects localDomain [] mempty Nothing $ do newCookie @_ @ZAuth.LU uid cid PersistentCookie mLabel RevokeSameLabel in cookie.cookieExpires === addUTCTime (fromIntegral defaultZAuthSettings.legalHoldUserTokenTimeout.legalHoldUserTokenTimeoutSeconds) defaultTime modifyMaxSuccess (const 3) . prop "cookie is persisted" $ \localDomain uid cid mLabel -> do - let Right (cky, sto) = runAllEffects localDomain [] Nothing $ do + let Right (cky, sto) = runAllEffects localDomain [] mempty Nothing $ do c <- newCookie @_ @ZAuth.LU uid cid PersistentCookie mLabel RevokeSameLabel s <- listCookies uid pure (c, s) @@ -409,7 +413,7 @@ spec = describe "AuthenticationSubsystem.Interpreter" do prop "old cookies with same label are revoked on insert" $ \localDomain uid cid typ mLabel otherLabel policy -> let (events, Right (cookie1, cookie2, cookie3, cookies)) = - runAllEffectsWithEventState localDomain [] Nothing $ + runAllEffectsWithEventState localDomain [] mempty Nothing $ (,,,) <$> newCookie @_ @ZAuth.U uid cid typ mLabel policy <*> newCookie @_ @ZAuth.U uid cid typ mLabel policy @@ -445,7 +449,7 @@ spec = describe "AuthenticationSubsystem.Interpreter" do \localDomain uidA uidB cid typ lab policy -> uidA /= uidB ==> let (events, Right (cookieA1, cookieB, cookieA2, cookiesA, cookiesB)) = - runAllEffectsWithEventState localDomain [] Nothing $ + runAllEffectsWithEventState localDomain [] mempty Nothing $ (,,,,) <$> newCookie @_ @ZAuth.U uidA cid typ (Just lab) policy <*> newCookie @_ @ZAuth.U uidB cid typ (Just lab) policy @@ -477,15 +481,6 @@ newtype Upto4 = Upto4 Int instance Arbitrary Upto4 where arbitrary = Upto4 <$> elements [0 .. 4] -verifyPasswordProp :: PlainTextPassword8 -> Maybe Password -> Property -verifyPasswordProp plainTextPassword passwordHash = - counterexample ("Password doesn't match, plainText=" <> show plainTextPassword <> ", passwordHash=" <> show passwordHash) $ - fmap (verifyPasswordPure plainTextPassword) passwordHash == Just True - -hashAndUpsertPassword :: (Member PasswordStore r) => UserId -> PlainTextPassword8 -> Sem r () -hashAndUpsertPassword uid password = - upsertHashedPassword uid =<< hashPassword password - expect1ResetPasswordEmail :: (Member (State (Map EmailAddress [SentMail])) r) => EmailAddress -> Sem r PasswordResetPair expect1ResetPasswordEmail email = getEmailsSentTo email diff --git a/libs/wire-subsystems/test/unit/Wire/EnterpriseLoginSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/EnterpriseLoginSubsystem/InterpreterSpec.hs index 359372c7ef7..1266cbc6434 100644 --- a/libs/wire-subsystems/test/unit/Wire/EnterpriseLoginSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/EnterpriseLoginSubsystem/InterpreterSpec.hs @@ -68,7 +68,7 @@ runDependencies :: Either EnterpriseLoginSubsystemError a runDependencies = run - . userSubsystemTestInterpreter [] + . runInMemoryUserSubsytemInterpreter mempty mempty . (evalState mempty . inMemoryUserKeyStoreInterpreter . raiseUnder) . fakeRpc . runRandomPure diff --git a/libs/wire-subsystems/test/unit/Wire/IdPSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/IdPSubsystem/InterpreterSpec.hs index f9a98d5e7c2..a0a1b772c8e 100644 --- a/libs/wire-subsystems/test/unit/Wire/IdPSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/IdPSubsystem/InterpreterSpec.hs @@ -32,6 +32,7 @@ import Polysemy import Polysemy.Error import Polysemy.State import SAML2.WebSSO qualified as SAML +import SAML2.WebSSO.Types (IdPMetadata (_edIssuer)) import System.Logger.Message qualified as Log import Test.Hspec import Test.Hspec.QuickCheck @@ -150,6 +151,9 @@ spec = describe "IdPSubsystem.Interpreter" $ do ( \otherIdP -> Just dom == otherIdP._idpExtraInfo._domain || idp._idpId == otherIdP._idpId + -- In reality, this constraint is enforced by `validateNewIdP`, called close to the endpoint. + -- Depending on the IdP API version, the issuer must be either unique per backend (V1) or per team (V2) + || idp._idpMetadata._edIssuer == otherIdP._idpMetadata._edIssuer ) otherIdPs ) diff --git a/libs/wire-subsystems/test/unit/Wire/MeetingsSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/MeetingsSubsystem/InterpreterSpec.hs index 003b8472841..5c45b433bc6 100644 --- a/libs/wire-subsystems/test/unit/Wire/MeetingsSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/MeetingsSubsystem/InterpreterSpec.hs @@ -35,6 +35,7 @@ import System.Random (StdGen, mkStdGen) import Test.Hspec import Test.Hspec.QuickCheck (prop) import Test.QuickCheck (counterexample, ioProperty, (.&&.), (===), (==>)) +import Text.Email.Parser (unsafeEmailAddress) import Wire.API.Meeting qualified as API import Wire.API.Team.Feature import Wire.API.Team.Member (TeamMember, mkTeamMember) @@ -423,3 +424,208 @@ spec = describe "MeetingsSubsystem.Interpreter" $ do .&&. m.startTime === effectiveStart .&&. m.endTime === effectiveEnd .&&. m.recurrence === fromMaybe baseMeeting.recurrence update.recurrence + + describe "addInvitedEmails" $ do + let now = UTCTime (fromGregorian 2026 1 1) 0 + gen = mkStdGen 42 + uid1 = Id $ read "00000000-0000-0000-0000-000000000001" + uid2 = Id $ read "00000000-0000-0000-0000-000000000002" + zUser1 = toLocalUnsafe (Domain "wire.com") uid1 + zUser2 = toLocalUnsafe (Domain "wire.com") uid2 + teamId = Id $ read "00000000-0000-0000-0000-000000000100" + teamMember1 = mkTeamMember uid1 fullPermissions Nothing UserLegalHoldDisabled + teamMember2 = mkTeamMember uid2 fullPermissions Nothing UserLegalHoldDisabled + teamConfig = + npUpdate @MeetingsPremiumConfig (LockableFeature FeatureStatusEnabled LockStatusUnlocked def) def + email1 = unsafeEmailAddress "user1" "example.com" + email2 = unsafeEmailAddress "user2" "example.com" + + it "returns True and adds emails for creator of valid meeting" $ do + let newMeeting = + API.NewMeeting + { title = fromJust $ checked "Test Meeting", + startTime = addUTCTime 3600 now, + endTime = addUTCTime 7200 now, + recurrence = Nothing, + invitedEmails = [] + } + + result <- runTestStack now gen Map.empty teamConfig $ do + (meeting, _conv) <- createMeeting zUser1 newMeeting + success <- addInvitedEmails zUser1 meeting.id [email1, email2] + fetched <- getMeeting zUser1 meeting.id + pure (success, fetched) + + case result of + Left err -> fail $ "Error: " <> show err + Right (success, Just m) -> do + success `shouldBe` True + m.invitedEmails `shouldBe` [email1, email2] + Right (_, Nothing) -> fail "Expected Just meeting" + + it "returns False for expired meeting" $ do + let newMeeting = + API.NewMeeting + { title = fromJust $ checked "Expired Meeting", + startTime = addUTCTime (-7200) now, + endTime = addUTCTime (-5000) now, + recurrence = Nothing, + invitedEmails = [] + } + + result <- runTestStack now gen Map.empty teamConfig $ do + (meeting, _conv) <- createMeeting zUser1 newMeeting + addInvitedEmails zUser1 meeting.id [email1] + + result `shouldBe` Right False + + it "returns False for non-creator" $ do + let newMeeting = + API.NewMeeting + { title = fromJust $ checked "Non-creator Test", + startTime = addUTCTime 3600 now, + endTime = addUTCTime 7200 now, + recurrence = Nothing, + invitedEmails = [] + } + + result <- runTestStack now gen (Map.singleton teamId [teamMember1, teamMember2]) teamConfig $ do + (meeting, _conv) <- createMeeting zUser1 newMeeting + addInvitedEmails zUser2 meeting.id [email1] + + result `shouldBe` Right False + + it "returns False for non-existent meeting" $ do + let nonExistentId = Qualified (Id $ read "00000000-0000-0000-0000-000000000999") (Domain "wire.com") + + result <- + runTestStack now gen Map.empty teamConfig $ + addInvitedEmails zUser1 nonExistentId [email1] + + result `shouldBe` Right False + + describe "removeInvitedEmails" $ do + let now = UTCTime (fromGregorian 2026 1 1) 0 + gen = mkStdGen 42 + uid1 = Id $ read "00000000-0000-0000-0000-000000000001" + uid2 = Id $ read "00000000-0000-0000-0000-000000000002" + zUser1 = toLocalUnsafe (Domain "wire.com") uid1 + zUser2 = toLocalUnsafe (Domain "wire.com") uid2 + teamId = Id $ read "00000000-0000-0000-0000-000000000100" + teamMember1 = mkTeamMember uid1 fullPermissions Nothing UserLegalHoldDisabled + teamMember2 = mkTeamMember uid2 fullPermissions Nothing UserLegalHoldDisabled + teamConfig = + npUpdate @MeetingsPremiumConfig (LockableFeature FeatureStatusEnabled LockStatusUnlocked def) def + email1 = unsafeEmailAddress "user1" "example.com" + email2 = unsafeEmailAddress "user2" "example.com" + email3 = unsafeEmailAddress "user3" "example.com" + + it "returns True and removes emails for creator of valid meeting" $ do + let newMeeting = + API.NewMeeting + { title = fromJust $ checked "Test Meeting", + startTime = addUTCTime 3600 now, + endTime = addUTCTime 7200 now, + recurrence = Nothing, + invitedEmails = [email1, email2, email3] + } + + result <- runTestStack now gen Map.empty teamConfig $ do + (meeting, _conv) <- createMeeting zUser1 newMeeting + success <- removeInvitedEmails zUser1 meeting.id [email2] + fetched <- getMeeting zUser1 meeting.id + pure (success, fetched) + + case result of + Left err -> fail $ "Error: " <> show err + Right (success, Just m) -> do + success `shouldBe` True + m.invitedEmails `shouldBe` [email1, email3] + Right (_, Nothing) -> fail "Expected Just meeting" + + it "returns True when removing all emails" $ do + let newMeeting = + API.NewMeeting + { title = fromJust $ checked "Test Meeting", + startTime = addUTCTime 3600 now, + endTime = addUTCTime 7200 now, + recurrence = Nothing, + invitedEmails = [email1, email2] + } + + result <- runTestStack now gen Map.empty teamConfig $ do + (meeting, _conv) <- createMeeting zUser1 newMeeting + success <- removeInvitedEmails zUser1 meeting.id [email1, email2] + fetched <- getMeeting zUser1 meeting.id + pure (success, fetched) + + case result of + Left err -> fail $ "Error: " <> show err + Right (success, Just m) -> do + success `shouldBe` True + m.invitedEmails `shouldBe` [] + Right (_, Nothing) -> fail "Expected Just meeting" + + it "returns True when removing non-existent emails" $ do + let newMeeting = + API.NewMeeting + { title = fromJust $ checked "Test Meeting", + startTime = addUTCTime 3600 now, + endTime = addUTCTime 7200 now, + recurrence = Nothing, + invitedEmails = [email1] + } + + result <- runTestStack now gen Map.empty teamConfig $ do + (meeting, _conv) <- createMeeting zUser1 newMeeting + success <- removeInvitedEmails zUser1 meeting.id [email2, email3] + fetched <- getMeeting zUser1 meeting.id + pure (success, fetched) + + case result of + Left err -> fail $ "Error: " <> show err + Right (success, Just m) -> do + success `shouldBe` True + m.invitedEmails `shouldBe` [email1] + Right (_, Nothing) -> fail "Expected Just meeting" + + it "returns False for expired meeting" $ do + let newMeeting = + API.NewMeeting + { title = fromJust $ checked "Expired Meeting", + startTime = addUTCTime (-7200) now, + endTime = addUTCTime (-5000) now, + recurrence = Nothing, + invitedEmails = [email1] + } + + result <- runTestStack now gen Map.empty teamConfig $ do + (meeting, _conv) <- createMeeting zUser1 newMeeting + removeInvitedEmails zUser1 meeting.id [email1] + + result `shouldBe` Right False + + it "returns False for non-creator" $ do + let newMeeting = + API.NewMeeting + { title = fromJust $ checked "Non-creator Test", + startTime = addUTCTime 3600 now, + endTime = addUTCTime 7200 now, + recurrence = Nothing, + invitedEmails = [email1, email2] + } + + result <- runTestStack now gen (Map.singleton teamId [teamMember1, teamMember2]) teamConfig $ do + (meeting, _conv) <- createMeeting zUser1 newMeeting + removeInvitedEmails zUser2 meeting.id [email1] + + result `shouldBe` Right False + + it "returns False for non-existent meeting" $ do + let nonExistentId = Qualified (Id $ read "00000000-0000-0000-0000-000000000999") (Domain "wire.com") + + result <- + runTestStack now gen Map.empty teamConfig $ + removeInvitedEmails zUser1 nonExistentId [email1] + + result `shouldBe` Right False diff --git a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs index cacc8f024b4..bfc15af38f7 100644 --- a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs +++ b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs @@ -80,6 +80,7 @@ import Wire.API.Allowlists (AllowlistEmailDomains) import Wire.API.Federation.API import Wire.API.Federation.Component import Wire.API.Federation.Error +import Wire.API.Password import Wire.API.Team.Collaborator import Wire.API.Team.Feature import Wire.API.Team.Member hiding (userId) @@ -89,6 +90,8 @@ import Wire.API.User.IdentityProvider import Wire.API.User.Password import Wire.ActivationCodeStore import Wire.AppStore +import Wire.AppSubsystem +import Wire.AppSubsystem.Interpreter import Wire.AuthenticationSubsystem import Wire.AuthenticationSubsystem.Config import Wire.AuthenticationSubsystem.Cookie.Limit @@ -208,13 +211,19 @@ instance Arbitrary ActiveStoredUser where type AllErrors = [ Error UserSubsystemError, + Error AppSubsystemError, Error FederationError, Error AuthenticationSubsystemError, Error RateLimitExceeded, Error TeamCollaboratorsError ] -type MiniBackendEffects = UserSubsystem ': TeamCollaboratorsSubsystem ': MiniBackendLowerEffects +type MiniBackendEffects = + AuthUserAppRecursiveEffects + `Append` '[TeamCollaboratorsSubsystem] + `Append` MiniBackendLowerEffects + +type AuthUserAppRecursiveEffects = '[AuthenticationSubsystem, UserSubsystem, AppSubsystem] ---------------------------------------------------------------------- -- lower effect interpreters (hierarchically) @@ -227,7 +236,8 @@ data MiniBackendParams r = MiniBackendParams localBackend :: MiniBackend, teams :: Map TeamId [TeamMember], galleyConfigs :: AllTeamFeatures, - cfg :: UserSubsystemConfig + usrCfg :: UserSubsystemConfig, + appCfg :: AppSubsystemConfig } -- | `MiniBackendLowerEffects` is not a long, flat list, but a tree of effects. This way we @@ -287,7 +297,7 @@ miniBackendLowerEffectsInterpreters mb@(MiniBackendParams {..}) = . maybeFederationAPIAccess . stateEffectsInterpreters mb . ignoreMetrics - . inputEffectsInterpreters cfg localBackend.teamIdps + . inputEffectsInterpreters usrCfg appCfg localBackend.teamIdps . interpretNowConst (UTCTime (ModifiedJulianDay 0) 0) . runRandomPure . runCryptoSignUnsafe @@ -325,6 +335,7 @@ type StateEffects = State (Map EmailKey (Maybe UserId, ActivationCode)), State [EmailKey], State [StoredUser], + State (Map UserId Password), State UserGroupInMemState, State [StoredApp], State UserIndex, @@ -347,6 +358,7 @@ stateEffectsInterpreters MiniBackendParams {..} = . liftIndexedUserStoreState . liftAppStoreState . liftUserGroupStoreState + . liftUserPasswordState . liftUserStoreState . liftBlockListStoreState . liftActivationCodeStoreState @@ -357,6 +369,7 @@ stateEffectsInterpreters MiniBackendParams {..} = type InputEffects = '[ Input UserSubsystemConfig, + Input AppSubsystemConfig, Input (Maybe AllowlistEmailDomains), Input (Map TeamId IdPList), Input AuthenticationSubsystemConfig, @@ -400,13 +413,20 @@ defaultAuthenticationSubsystemConfig = defaultLocalDomain :: Local () defaultLocalDomain = (toLocalUnsafe (Domain "localdomain") ()) -inputEffectsInterpreters :: forall r a. UserSubsystemConfig -> Map TeamId IdPList -> Sem (InputEffects `Append` r) a -> Sem r a -inputEffectsInterpreters cfg teamIdps = +inputEffectsInterpreters :: + forall r a. + UserSubsystemConfig -> + AppSubsystemConfig -> + Map TeamId IdPList -> + Sem (InputEffects `Append` r) a -> + Sem r a +inputEffectsInterpreters usrCfg appCfg teamIdps = runInputConst defaultLocalDomain . runInputConst defaultAuthenticationSubsystemConfig . runInputConst teamIdps . runInputConst Nothing - . runInputConst cfg + . runInputConst appCfg + . runInputConst usrCfg ---------------------------------------------------------------------- @@ -415,6 +435,7 @@ data MiniBackend = MkMiniBackend { -- | this is morally the same as the users stored in the actual backend -- invariant: for each key, the user.id and the key are the same users :: [StoredUser], + userPasswords :: Map UserId Password, apps :: [StoredApp], userIndex :: UserIndex, userKeys :: Map EmailKey UserId, @@ -428,12 +449,13 @@ data MiniBackend = MkMiniBackend pushNotifications :: [Push], userGroups :: UserGroupInMemState } - deriving stock (Eq, Show, Generic) + deriving stock (Show, Generic) instance Default MiniBackend where def = MkMiniBackend { users = mempty, + userPasswords = mempty, apps = mempty, userIndex = emptyIndex, userKeys = mempty, @@ -532,17 +554,10 @@ miniGetAllProfiles :: Sem r [UserProfile] miniGetAllProfiles = do users <- gets (.users) - apps <- gets (.apps) dom <- input pure $ map - ( \u -> - let userType - | any ((== u.id) . (.id)) apps = UserTypeApp - | isJust u.serviceId = UserTypeBot - | otherwise = UserTypeRegular - in mkUserProfileWithEmail Nothing userType (mkUserFromStored dom miniLocale u) defUserLegalHoldStatus - ) + (\u -> mkUserProfileWithEmail Nothing (mkUserFromStored dom miniLocale u) Nothing defUserLegalHoldStatus) users miniGetUsersByIds :: [UserId] -> MiniFederationMonad 'Brig [UserProfile] @@ -602,14 +617,14 @@ interpretFederationStackState :: UserSubsystemConfig -> Sem (MiniBackendEffects `Append` r) a -> Sem r (MiniBackend, a) -interpretFederationStackState localBackend backends teams cfg = +interpretFederationStackState localBackend backends teams usrCfg = interpretMaybeFederationStackState MiniBackendParams { maybeFederationAPIAccess = (miniFederationAPIAccess backends), localBackend = localBackend, - teams = teams, galleyConfigs = def, - cfg = cfg + appCfg = def, + .. } runNoFederationStack :: @@ -635,7 +650,7 @@ runNoFederationStackUserSubsystemErrorEither localBackend teams cfg = run . userSubsystemErrorEitherUnsafe . interpretNoFederationStack localBackend teams def cfg userSubsystemErrorEitherUnsafe :: Sem AllErrors a -> Sem '[] (Either UserSubsystemError a) -userSubsystemErrorEitherUnsafe = runErrorUnsafe . runErrorUnsafe . runErrorUnsafe . runErrorUnsafe . runError +userSubsystemErrorEitherUnsafe = runErrorUnsafe . runErrorUnsafe . runErrorUnsafe . runErrorUnsafe . runErrorUnsafe . runError interpretNoFederationStack :: (Members AllErrors r) => @@ -656,29 +671,44 @@ interpretNoFederationStackState :: UserSubsystemConfig -> Sem (MiniBackendEffects `Append` r) a -> Sem r (MiniBackend, a) -interpretNoFederationStackState localBackend teams galleyConfigs cfg = +interpretNoFederationStackState localBackend teams galleyConfigs usrCfg = interpretMaybeFederationStackState MiniBackendParams { maybeFederationAPIAccess = emptyFederationAPIAcesss, localBackend = localBackend, - teams = teams, galleyConfigs = galleyConfigs, - cfg = cfg + appCfg = def, + .. } interpretMaybeFederationStackState :: - forall r a. (Members AllErrors r) => MiniBackendParams r -> Sem (MiniBackendEffects `Append` r) a -> Sem r (MiniBackend, a) interpretMaybeFederationStackState mb = - let authSubsystemInterpreter :: InterpreterFor AuthenticationSubsystem (TeamCollaboratorsSubsystem ': MiniBackendLowerEffects `Append` r) - authSubsystemInterpreter = interpretAuthenticationSubsystem userSubsystemInterpreter + miniBackendLowerEffectsInterpreters mb . interpretTeamCollaboratorsSubsystem . runRecursiveAuthUserApp + +-- FUTUREWORK(fisx): it would be nice to have a definition of an +-- interpreter of all the subsystems combined, but since the +-- individual effect interpreters will have different needs for +-- lower-level effects, and since our effect stacks are generally +-- rather ad hoc, this is not a trivial task. So for now we just +-- diplicate this function whenever we need it. +runRecursiveAuthUserApp :: + (Members AllErrors r, Members (TeamCollaboratorsSubsystem ': MiniBackendLowerEffects) r) => + Sem (AuthenticationSubsystem ': UserSubsystem ': AppSubsystem ': r) a -> + Sem r a +runRecursiveAuthUserApp = runApp . runUser . runAuth + where + runAuth :: forall r. (Members AllErrors r, Members (TeamCollaboratorsSubsystem ': MiniBackendLowerEffects) r) => InterpreterFor AuthenticationSubsystem r + runAuth = interpretAuthenticationSubsystem runUser - userSubsystemInterpreter :: InterpreterFor UserSubsystem (TeamCollaboratorsSubsystem ': MiniBackendLowerEffects `Append` r) - userSubsystemInterpreter = runUserSubsystem authSubsystemInterpreter - in miniBackendLowerEffectsInterpreters mb . interpretTeamCollaboratorsSubsystem . userSubsystemInterpreter + runUser :: forall r. (Members AllErrors r, Members (TeamCollaboratorsSubsystem ': MiniBackendLowerEffects) r) => InterpreterFor UserSubsystem r + runUser = runUserSubsystem runAuth runApp + + runApp :: forall r. (Members AllErrors r, Members (TeamCollaboratorsSubsystem ': MiniBackendLowerEffects) r) => InterpreterFor AppSubsystem r + runApp = runAppSubsystem runUser runAuth liftInvitationInfoStoreState :: (Member (State MiniBackend) r) => Sem (State (Map InvitationCode StoredInvitation) : r) a -> Sem r a liftInvitationInfoStoreState = interpret \case @@ -720,6 +750,11 @@ liftUserStoreState = interpret $ \case Polysemy.State.Get -> gets (.users) Put newUsers -> modify $ \b -> (b :: MiniBackend) {users = newUsers} +liftUserPasswordState :: (Member (State MiniBackend) r) => Sem (State (Map UserId Password) : r) a -> Sem r a +liftUserPasswordState = interpret $ \case + Polysemy.State.Get -> gets (.userPasswords) + Put newPasswords -> modify $ \b -> (b :: MiniBackend) {userPasswords = newPasswords} + liftAppStoreState :: (Member (State MiniBackend) r) => Sem (State [StoredApp] : r) a -> Sem r a liftAppStoreState = interpret $ \case Polysemy.State.Get -> gets (.apps) @@ -736,7 +771,7 @@ liftIndexedUserStoreState = interpret $ \case Put newUserIndex -> modify $ \b -> (b :: MiniBackend) {userIndex = newUserIndex} runAllErrorsUnsafe :: forall a. (HasCallStack) => Sem AllErrors a -> a -runAllErrorsUnsafe = run . runErrorUnsafe . runErrorUnsafe . runErrorUnsafe . runErrorUnsafe . runErrorUnsafe +runAllErrorsUnsafe = run . runErrorUnsafe . runErrorUnsafe . runErrorUnsafe . runErrorUnsafe . runErrorUnsafe . runErrorUnsafe emptyFederationAPIAcesss :: InterpreterFor (FederationAPIAccess MiniFederationMonad) r emptyFederationAPIAcesss = interpret $ \case diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/HashPassword.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/HashPassword.hs index 07dc9906328..6caf8390d18 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/HashPassword.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/HashPassword.hs @@ -30,13 +30,13 @@ import Wire.HashPassword.Interpreter staticHashPasswordInterpreter :: InterpreterFor HashPassword r staticHashPasswordInterpreter = interpret $ \case - HashPassword6 password -> hashPassword password - HashPassword8 password -> hashPassword password + HashPassword6 password -> pure $ hashPassword password + HashPassword8 password -> pure $ hashPassword password VerifyPasswordWithStatus plain hashed -> pure $ verifyPasswordWithStatusImpl PasswordHashingScrypt plain hashed -hashPassword :: (Monad m) => PlainTextPassword' t -> m Password +hashPassword :: PlainTextPassword' t -> Password hashPassword password = - pure . Argon2Password $ + Argon2Password $ hashPasswordArgon2idWithSalt fastArgon2IdOptions "9bytesalt" diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/MeetingsStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/MeetingsStore.hs index 0c770390fc0..a602a64d443 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/MeetingsStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/MeetingsStore.hs @@ -18,6 +18,7 @@ module Wire.MockInterpreters.MeetingsStore where import Data.Id +import Data.List qualified as List import Data.Map qualified as Map import Imports import Polysemy @@ -67,3 +68,37 @@ inMemoryMeetingsStoreInterpreter = interpret $ \case updatedAt = now } modify (Map.insert mid updatedMeeting) >> pure (Just updatedMeeting) + ListMeetingsByUser userId cutoffTime -> + gets $ + filter (\sm -> sm.creator == userId && sm.endTime >= cutoffTime) + . List.sortOn (.startTime) + . Map.elems + ListMeetingsByConversation convId cutoffTime -> + gets $ + filter (\sm -> sm.conversationId == convId && sm.endTime >= cutoffTime) + . List.sortOn (.startTime) + . Map.elems + AddInvitedEmails mid newEmails -> do + sm <- gets (Map.lookup mid) + case sm of + Nothing -> pure () + Just meeting -> do + now <- Now.get + let updatedMeeting = + meeting + { invitedEmails = meeting.invitedEmails ++ newEmails, + updatedAt = now + } + modify (Map.insert mid updatedMeeting) + RemoveInvitedEmails mid emailsToRemove -> do + sm <- gets (Map.lookup mid) + case sm of + Nothing -> pure () + Just meeting -> do + now <- Now.get + let updatedMeeting = + meeting + { invitedEmails = filter (`notElem` emailsToRemove) meeting.invitedEmails, + updatedAt = now + } + modify (Map.insert mid updatedMeeting) diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/PasswordStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/PasswordStore.hs index 72ddd6277ff..33952587697 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/PasswordStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/PasswordStore.hs @@ -18,7 +18,6 @@ module Wire.MockInterpreters.PasswordStore where import Data.Id -import Data.Map qualified as Map import Imports import Polysemy import Polysemy.State @@ -28,8 +27,6 @@ import Wire.PasswordStore runInMemoryPasswordStoreInterpreter :: InterpreterFor PasswordStore r runInMemoryPasswordStoreInterpreter = evalState (mempty :: Map UserId Password) . inMemoryPasswordStoreInterpreter . raiseUnder -inMemoryPasswordStoreInterpreter :: (Member (State (Map UserId Password)) r) => InterpreterFor PasswordStore r +inMemoryPasswordStoreInterpreter :: InterpreterFor PasswordStore r inMemoryPasswordStoreInterpreter = interpret $ \case - UpsertHashedPassword uid password -> modify $ Map.insert uid password - LookupHashedPassword uid -> gets $ Map.lookup uid LookupHashedProviderPassword _uid -> error ("Implement as needed" :: String) diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserKeyStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserKeyStore.hs index eef306b861b..10f25b16367 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserKeyStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserKeyStore.hs @@ -19,11 +19,24 @@ module Wire.MockInterpreters.UserKeyStore where import Data.Id import Data.Map qualified as M +import Data.Map qualified as Map import Imports import Polysemy import Polysemy.State +import Wire.StoredUser import Wire.UserKeyStore +runInMemoryUserKeyStoreIntepreterWithStoredUsers :: [StoredUser] -> InterpreterFor UserKeyStore r +runInMemoryUserKeyStoreIntepreterWithStoredUsers initialUsers = + let emailKeys = Map.fromList $ mapMaybe (\u -> (,u.id) . mkEmailKey <$> u.email) initialUsers + in runInMemoryUserKeyStoreIntepreter emailKeys + +runInMemoryUserKeyStoreIntepreter :: Map EmailKey UserId -> InterpreterFor UserKeyStore r +runInMemoryUserKeyStoreIntepreter keys = + evalState keys + . inMemoryUserKeyStoreInterpreter + . raiseUnder + inMemoryUserKeyStoreInterpreter :: (Member (State (Map EmailKey UserId)) r) => InterpreterFor UserKeyStore r diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs index ae743e865be..9f6d24d9ef1 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs @@ -19,14 +19,17 @@ module Wire.MockInterpreters.UserStore where +import Control.Monad.Trans.Maybe (MaybeT (..)) import Data.Handle import Data.Id +import Data.Map qualified as Map import Data.Time import Data.Time.Calendar.OrdinalDate import Imports import Polysemy import Polysemy.Error import Polysemy.State +import Wire.API.Password import Wire.API.User hiding (DeleteUser) import Wire.API.User qualified as User import Wire.API.User.Search (SetSearchable (SetSearchable)) @@ -34,14 +37,27 @@ import Wire.StoredUser import Wire.UserStore import Wire.UserStore.IndexUser +runInMemoryUserStoreInterpreter :: [StoredUser] -> Map UserId Password -> InterpreterFor UserStore r +runInMemoryUserStoreInterpreter users passwords = + evalState users + . evalState passwords + . inMemoryUserStoreInterpreter + . raiseUnder + . raiseUnder + inMemoryUserStoreInterpreter :: forall r. - (Member (State [StoredUser]) r) => + ( Member (State [StoredUser]) r, + Member (State (Map UserId Password)) r + ) => InterpreterFor UserStore r inMemoryUserStoreInterpreter = interpret $ \case - CreateUser new _ -> modify (newStoredUserToStoredUser new :) - GetUsers uids -> gets $ filter (\user -> user.id `elem` uids) - DoesUserExist uid -> gets (any (\u -> u.id == uid)) + CreateUser new _ -> do + modify (newStoredUserToStoredUser new :) + forM_ new.password $ modify . Map.insert new.id + GetUsers uids -> do + gets $ filter (\user -> user.id `elem` uids) + DoesUserExist uid -> gets @[StoredUser] (any (\u -> u.id == uid)) UpdateUser uid update -> modify (map doUpdate) where doUpdate :: StoredUser -> StoredUser @@ -79,7 +95,7 @@ inMemoryUserStoreInterpreter = interpret $ \case else u UpdateSSOId uid ssoId -> do updateUserInStore uid (\u -> u {ssoId = ssoId}) - gets (any (\u -> u.id == uid)) + gets @[StoredUser] (any (\u -> u.id == uid)) UpdateManagedBy uid managedBy -> updateUserInStore uid (\u -> u {managedBy = Just managedBy}) UpdateAccountStatus uid accountStatus -> updateUserInStore uid (\u -> u {status = Just accountStatus}) ActivateUser uid identity -> updateUserInStore uid (\u -> u {activated = True, email = emailIdentity identity}) @@ -112,7 +128,7 @@ inMemoryUserStoreInterpreter = interpret $ \case us' <- f us put us' DeleteUser user -> modify @[StoredUser] $ filter (\u -> u.id /= User.userId user) - LookupName uid -> (.name) <$$> gets (find $ \u -> u.id == uid) + LookupName uid -> (.name) <$$> gets @[StoredUser] (find $ \u -> u.id == uid) LookupHandle h -> lookupHandleImpl h GlimpseHandle h -> lookupHandleImpl h LookupStatus uid -> lookupStatusImpl uid @@ -125,7 +141,14 @@ inMemoryUserStoreInterpreter = interpret $ \case GetRichInfo _ -> error "GetRichInfo: not implemented" LookupRichInfos _ -> error "LookupRichInfos: not implemented" UpdateRichInfo {} -> error "UpdateRichInfo: Not implemented" - GetUserAuthenticationInfo _uid -> error "Not implemented" + UpsertHashedPassword uid pw -> + modify $ Map.insert uid pw + LookupHashedPassword uid -> + gets $ Map.lookup uid + GetUserAuthenticationInfo uid -> runMaybeT $ do + status <- MaybeT $ lookupStatusImpl uid + pw <- lift $ gets @(Map UserId Password) $ Map.lookup uid + pure (pw, status) DeleteEmail uid -> modify (map doUpdate) where doUpdate :: StoredUser -> StoredUser @@ -151,6 +174,7 @@ storedUserToIndexUser storedUser = let defaultTime = UTCTime (YearDay 0 1) 0 in IndexUser { userId = storedUser.id, + userType = inferUserType storedUser.serviceId storedUser.userType, teamId = storedUser.teamId, name = storedUser.name, accountStatus = storedUser.status, @@ -193,33 +217,6 @@ lookupHandleImpl h = do fmap (.id) . find ((== Just h) . (.handle)) -newStoredUserToStoredUser :: NewStoredUser -> StoredUser -newStoredUserToStoredUser new = - StoredUser - { id = new.id, - userType = Just new.userType, - name = new.name, - textStatus = new.textStatus, - pict = Just new.pict, - email = new.email, - emailUnvalidated = new.email, - ssoId = new.ssoId, - accentId = new.accentId, - assets = Just new.assets, - activated = new.activated, - status = Just new.status, - expires = new.expires, - language = Just new.language, - country = new.country, - providerId = new.providerId, - serviceId = new.serviceId, - handle = new.handle, - teamId = new.teamId, - managedBy = Just new.managedBy, - supportedProtocols = Just new.supportedProtocols, - searchable = Just new.searchable - } - updateUserInStore :: (Member (State [StoredUser]) r) => UserId -> (StoredUser -> StoredUser) -> Sem r () updateUserInStore uid f = modify (map doUpdate) where diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs index d2d1855cc3c..caf83cc4f7b 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs @@ -17,35 +17,59 @@ module Wire.MockInterpreters.UserSubsystem where +import Control.Monad.Trans.Maybe (MaybeT (..)) +import Data.Domain +import Data.Id +import Data.LanguageCodes import Data.LegalHold import Data.Qualified import Imports import Polysemy +import Wire.API.Password import Wire.API.User +import Wire.MockInterpreters.UserKeyStore +import Wire.MockInterpreters.UserStore +import Wire.StoredUser +import Wire.UserKeyStore (UserKeyStore) +import Wire.UserKeyStore qualified as UserKeyStore +import Wire.UserStore (UserStore) +import Wire.UserStore qualified as UserStore import Wire.UserSubsystem -userSubsystemTestInterpreter :: [User] -> InterpreterFor UserSubsystem r -userSubsystemTestInterpreter initialUsers = +runInMemoryUserSubsytemInterpreter :: [StoredUser] -> Map UserId Password -> InterpreterFor UserSubsystem r +runInMemoryUserSubsytemInterpreter initialUsers passwords = + runInMemoryUserStoreInterpreter initialUsers passwords + . runInMemoryUserKeyStoreIntepreterWithStoredUsers initialUsers + . inMemoryUserSubsystemInterpreter + . raiseUnder + . raiseUnder + +testDomain :: Domain +testDomain = Domain "test.example" + +testLocale :: Locale +testLocale = Locale (Language EN) Nothing + +inMemoryUserSubsystemInterpreter :: (Member UserStore r, Member UserKeyStore r) => InterpreterFor UserSubsystem r +inMemoryUserSubsystemInterpreter = interpret \case - GetAccountsByEmailNoFilter (tUnqualified -> emails) -> - pure $ - filter - (\u -> userEmail u `elem` (Just <$> emails)) - initialUsers - GetUserTeam uid -> pure $ do - user <- find (\u -> userId u == uid) initialUsers - user.userTeam - GetSelfProfile uid -> - pure . fmap SelfProfile $ - find (\u -> qUnqualified u.userQualifiedId == tUnqualified uid) initialUsers + GetAccountsByEmailNoFilter (tUnqualified -> emails) -> do + uids <- catMaybes <$> traverse (UserKeyStore.lookupKey . UserKeyStore.mkEmailKey) emails + storedUsers <- UserStore.getUsers uids + pure $ mkUserFromStored testDomain testLocale <$> storedUsers + GetUserTeam uid -> runMaybeT do + user <- MaybeT $ UserStore.getUser uid + MaybeT $ pure user.teamId + GetSelfProfile uid -> do + SelfProfile . mkUserFromStored testDomain testLocale <$$> UserStore.getUser (tUnqualified uid) IsBlocked _ -> pure False GetUserProfiles _ _ -> error "GetUserProfiles: implement on demand (userSubsystemInterpreter)" GetUserProfilesWithErrors _ _ -> error "GetUserProfilesWithErrors: implement on demand (userSubsystemInterpreter)" - GetLocalUserProfiles luids -> - let uids = qUnqualified $ tUntagged luids - in pure (toProfile <$> filter (\u -> userId u `elem` uids) initialUsers) + GetLocalUserProfilesFiltered upf luids -> case upf of + Everything -> toProfile . mkUserFromStored testDomain testLocale <$$> UserStore.getUsers (tUnqualified luids) + _ -> error "GetLocalUserProfilesFiltered : unsupported filter (userSubsystemInterpreter)" GetAccountsBy (tUnqualified -> GetBy NoPendingInvitations True True uids []) -> - pure (filter (\u -> userId u `elem` uids) initialUsers) + mkUserFromStored testDomain testLocale <$$> UserStore.getUsers uids GetAccountsBy _ -> error "GetAccountsBy: implement on demand (userSubsystemInterpreter)" UpdateUserProfile {} -> error "UpdateUserProfile: implement on demand (userSubsystemInterpreter)" CheckHandle _ -> error "CheckHandle: implement on demand (userSubsystemInterpreter)" @@ -68,4 +92,4 @@ userSubsystemTestInterpreter initialUsers = SetUserSearchable {} -> error "SetUserSearchable: implement on demand (userSubsystemInterpreter)" toProfile :: User -> UserProfile -toProfile u = mkUserProfileWithEmail (userEmail u) UserTypeRegular u UserLegalHoldDisabled +toProfile u = mkUserProfileWithEmail (userEmail u) u Nothing UserLegalHoldDisabled diff --git a/libs/wire-subsystems/test/unit/Wire/SAMLEmailSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/SAMLEmailSubsystem/InterpreterSpec.hs index 14d8a94ef3c..31ebb431d53 100644 --- a/libs/wire-subsystems/test/unit/Wire/SAMLEmailSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/SAMLEmailSubsystem/InterpreterSpec.hs @@ -24,6 +24,7 @@ import Test.QuickCheck import Text.Email.Parser (unsafeEmailAddress) import URI.ByteString import Wire.API.Locale +import Wire.API.Password import Wire.API.Routes.Internal.Brig (IdpChangedNotification (..)) import Wire.API.Team.Member import Wire.API.Team.Permission (fullPermissions) @@ -341,6 +342,7 @@ runInterpreters :: Email.EmailSubsystem, UserStore, State [StoredUser], + State (Map UserId Password), GalleyAPIAccess, Logger (Logger.Msg -> Logger.Msg), EmailSending, @@ -351,13 +353,14 @@ runInterpreters :: IO ([Mail], [(Level, LByteString)], a) runInterpreters users teamMap teamTemplates branding action = do lr <- newLogRecorder - (mails, (_userState, res)) <- + (mails, res) <- runM . runState @[Mail] [] -- Use runState to capture and return the Mail state . recordingEmailSendingInterpreter . recordLogs lr . miniGalleyAPIAccess teamMap def - . runState @[StoredUser] users + . evalState @(Map UserId Password) mempty + . evalState @[StoredUser] users . inMemoryUserStoreInterpreter . emailSubsystemInterpreter undefined teamTemplates branding . interpretTeamSubsystemToGalleyAPI diff --git a/libs/wire-subsystems/test/unit/Wire/ScimSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/ScimSubsystem/InterpreterSpec.hs index b33de25637f..3cdf8408a41 100644 --- a/libs/wire-subsystems/test/unit/Wire/ScimSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/ScimSubsystem/InterpreterSpec.hs @@ -42,8 +42,10 @@ import Wire.API.User as User import Wire.API.User.Scim import Wire.API.UserGroup import Wire.BrigAPIAccess (BrigAPIAccess (..)) +import Wire.MockInterpreters import Wire.ScimSubsystem import Wire.ScimSubsystem.Interpreter +import Wire.StoredUser import Wire.UserGroupSubsystem qualified as UGS import Wire.UserGroupSubsystem.Interpreter qualified as UGS import Wire.UserGroupSubsystem.InterpreterSpec qualified as UGS @@ -60,17 +62,17 @@ type AllDependencies = runDependencies :: forall a. (HasCallStack) => - [User] -> + [StoredUser] -> Map TeamId [TeamMember] -> Sem AllDependencies a -> Either ScimSubsystemError a runDependencies initialUsers initialTeams = - either (error . show) id . runDependenciesSafe initialUsers initialTeams + either (error . show) Imports.id . runDependenciesSafe initialUsers initialTeams runDependenciesSafe :: forall a. (HasCallStack) => - [User] -> + [StoredUser] -> Map TeamId [TeamMember] -> Sem AllDependencies a -> Either UGS.UserGroupSubsystemError (Either ScimSubsystemError a) @@ -88,12 +90,12 @@ runDependenciesSafe initialUsers initialTeams = scimBaseUri = Common.URI . fromJust . parseURI $ "http://nowhere.net/scim/v2" -- Mock BrigAPIAccess interpreter for tests - mockBrigAPIAccess :: (Member UGS.UserGroupSubsystem r) => [User] -> InterpreterFor BrigAPIAccess r + mockBrigAPIAccess :: (Member UGS.UserGroupSubsystem r) => [StoredUser] -> InterpreterFor BrigAPIAccess r mockBrigAPIAccess users = interpret $ \case CreateGroupInternal managedBy teamId creatorUserId newGroup -> do Right <$> UGS.createGroupInternal managedBy teamId creatorUserId newGroup GetAccountsBy getBy -> do - pure $ filter (\u -> User.userId u `elem` getBy.getByUserId) users + pure . map (mkUserFromStored testDomain testLocale) $ filter (\u -> u.id `elem` getBy.getByUserId) users GetGroupInternal tid gid False -> do UGS.getGroupInternal tid gid False DeleteGroupInternal managedBy teamId groupId -> @@ -110,8 +112,8 @@ instance Arbitrary Group.Group where members = [] } -mkScimGroupMember :: User -> Group.Member -mkScimGroupMember (idToText . User.userId -> value) = +mkScimGroupMember :: StoredUser -> Group.Member +mkScimGroupMember (idToText . (.id) -> value) = let typ = "User" ref = "$schema://$host.$domain/scim/vs/Users/$uuid" -- not a real URI, just a string for testing. in Group.Member {..} @@ -123,7 +125,7 @@ spec = UGS.timeoutHook $ describe "ScimSubsystem.Interpreter" $ do let newScimGroup = newScimGroup_ { Group.members = - let scimMembers = filter (\u -> u.userManagedBy == ManagedByScim) (UGS.allUsers team) + let scimMembers = filter (\u -> u.managedBy == Just ManagedByScim) (UGS.allUsers team) in mkScimGroupMember <$> scimMembers } resultOrError = do @@ -161,18 +163,18 @@ spec = UGS.timeoutHook $ describe "ScimSubsystem.Interpreter" $ do scimCreateUserGroup team.tid newScimGroup want = - if all (\u -> u.userManagedBy == ManagedByScim) groupMembers + if all (\u -> u.managedBy == Just ManagedByScim) groupMembers then isRight else isLeft unless (want have) do - expectationFailure . show $ ((.userManagedBy) <$> UGS.allUsers team) + expectationFailure . show $ ((.managedBy) <$> UGS.allUsers team) describe "scimDeleteUserGroup" $ do prop "deletes a SCIM-managed group" $ \(team :: UGS.ArbitraryTeam) (newScimGroup_ :: Group.Group) -> let newScimGroup = newScimGroup_ { Group.members = - let scimUsers = filter (\u -> u.userManagedBy == ManagedByScim) (UGS.allUsers team) + let scimUsers = filter (\u -> u.managedBy == Just ManagedByScim) (UGS.allUsers team) in mkScimGroupMember <$> scimUsers } resultOrError = do @@ -190,7 +192,7 @@ spec = UGS.timeoutHook $ describe "ScimSubsystem.Interpreter" $ do it "fails to delete non-SCIM-managed groups" $ do team :: UGS.ArbitraryTeam <- generate arbitrary - let ugName = either (error . show) id $ userGroupNameFromText "test-group" + let ugName = either (error . show) Imports.id $ userGroupNameFromText "test-group" let newGroup = NewUserGroup {name = ugName, members = mempty} let have = runDependenciesSafe (UGS.allUsers team) (UGS.galleyTeam team) $ do diff --git a/libs/wire-subsystems/test/unit/Wire/TeamInvitationSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/TeamInvitationSubsystem/InterpreterSpec.hs index 67bbbba7b54..dd2cf24f721 100644 --- a/libs/wire-subsystems/test/unit/Wire/TeamInvitationSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/TeamInvitationSubsystem/InterpreterSpec.hs @@ -50,12 +50,14 @@ import Wire.MockInterpreters import Wire.Sem.Logger.TinyLog import Wire.Sem.Now (Now) import Wire.Sem.Random +import Wire.StoredUser import Wire.TeamInvitationSubsystem import Wire.TeamInvitationSubsystem.Error import Wire.TeamInvitationSubsystem.Interpreter import Wire.TeamSubsystem import Wire.TeamSubsystem.GalleyAPI import Wire.UserKeyStore +import Wire.UserStore (UserStore) import Wire.UserSubsystem import Wire.Util @@ -75,12 +77,14 @@ type AllEffects = State UTCTime, EmailSubsystem, State (Map EmailAddress [SentMail]), - UserSubsystem + UserSubsystem, + UserStore, + UserKeyStore ] data RunAllEffectsArgs = RunAllEffectsArgs { teams :: Map TeamId [TeamMember], - initialUsers :: [User], + initialUsers :: [StoredUser], constGuardResult :: Maybe DomainRegistration } deriving (Eq, Show) @@ -88,7 +92,9 @@ data RunAllEffectsArgs = RunAllEffectsArgs runAllEffects :: RunAllEffectsArgs -> Sem AllEffects a -> Either TeamInvitationSubsystemError a runAllEffects args = run - . userSubsystemTestInterpreter args.initialUsers + . runInMemoryUserKeyStoreIntepreterWithStoredUsers args.initialUsers + . runInMemoryUserStoreInterpreter args.initialUsers mempty + . inMemoryUserSubsystemInterpreter . evalState mempty . noopEmailSubsystemInterpreter . evalState defaultTime @@ -111,30 +117,38 @@ spec = do prop "honors dommain config from `brig.domain_registration`" $ \(tid :: TeamId) (preDomRegUpd :: DomainRegistrationUpdate) - (preInviter :: User) + (preInviter :: StoredUser) (inviterEmail :: EmailAddress) (inviteeEmail :: EmailAddress) - (preExistingPersonalAccount :: Maybe User) + (preExistingPersonalAccount :: Maybe StoredUser) (preRegisteredDomain {- if Nothing, use invitee's email domain -} :: Maybe Domain) (sameTeam {- team id matches the team id in the domain registration -} :: Bool) -> let -- prepare the pre* prop args -- - domRegUpd = preDomRegUpd & if sameTeam then setTeamId else id + domRegUpd = preDomRegUpd & if sameTeam then setTeamId else Imports.id where setTeamId upd = case upd.teamInvite of Team _ -> DomainRegistrationUpdate upd.domainRedirect (Team tid) _ -> upd - inviter = preInviter {userIdentity = Just $ EmailIdentity inviterEmail} + inviter = + preInviter + { email = Just inviterEmail, + activated = True, + status = Just Active + } :: + StoredUser existingPersonalAccount = preExistingPersonalAccount <&> \r -> r - { userIdentity = Just $ EmailIdentity inviteeEmail, - userStatus = Active, - userTeam = Nothing, - userManagedBy = ManagedByWire - } + { email = Just inviteeEmail, + activated = True, + status = Just Active, + teamId = Nothing, + managedBy = Just ManagedByWire + } :: + StoredUser registeredDomain :: Domain registeredDomain = fromMaybe edom preRegisteredDomain @@ -150,8 +164,8 @@ spec = do blockedDomains = HashSet.empty } - inviterUid = qUnqualified inviter.userQualifiedId - inviterLuid = let domain = qDomain inviter.userQualifiedId in toLocalUnsafe domain inviterUid + inviterUid = inviter.id + inviterLuid = toLocalUnsafe testDomain inviterUid inviterMember = mkTeamMember inviterUid fullPermissions Nothing UserLegalHoldDisabled invReq = @@ -211,14 +225,21 @@ spec = do prop "try to invite to blocked domain" $ \(tid :: TeamId) - (preExistingPersonalAccount :: Maybe User) + (preExistingPersonalAccount :: Maybe StoredUser) (preExistingInviteeEmail :: EmailAddress) + (inviterNoEmail :: StoredUser) + (inviterEmail :: EmailAddress) (emailUsername :: EmailUsername) (blockedDomains :: NonEmptyList Domain) -> do - let hasEmailIdentity user = isJust $ emailIdentity =<< userIdentity user + let inviter = + inviterNoEmail + { email = Just inviterEmail, + status = Just Active, + activated = True + } :: + StoredUser blockedEmailDomain <- anyElementOf blockedDomains - inviter <- arbitrary @User `suchThat` hasEmailIdentity let blockedEmailAddress :: EmailAddress = unsafeEmailAddress @@ -241,18 +262,19 @@ spec = do blockedDomains = (HashSet.fromList . getNonEmpty) blockedDomains } - inviterUid = qUnqualified inviter.userQualifiedId - inviterLuid = let domain = qDomain inviter.userQualifiedId in toLocalUnsafe domain inviterUid + inviterUid = inviter.id + inviterLuid = toLocalUnsafe testDomain inviterUid inviterMember = mkTeamMember inviterUid fullPermissions Nothing UserLegalHoldDisabled existingPersonalAccount = preExistingPersonalAccount <&> \r -> r - { userIdentity = Just $ EmailIdentity preExistingInviteeEmail, - userStatus = Active, - userTeam = Nothing, - userManagedBy = ManagedByWire - } + { email = Just preExistingInviteeEmail, + status = Just Active, + teamId = Nothing, + managedBy = Just ManagedByWire + } :: + StoredUser interpreterArgs = RunAllEffectsArgs diff --git a/libs/wire-subsystems/test/unit/Wire/UserGroupSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserGroupSubsystem/InterpreterSpec.hs index 019cd848e36..c4288743fee 100644 --- a/libs/wire-subsystems/test/unit/Wire/UserGroupSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/UserGroupSubsystem/InterpreterSpec.hs @@ -61,6 +61,7 @@ import Wire.MockInterpreters as Mock import Wire.NotificationSubsystem import Wire.Sem.Random qualified as Random import Wire.Sem.Random.Null qualified as Random +import Wire.StoredUser import Wire.TeamSubsystem import Wire.TeamSubsystem.GalleyAPI import Wire.UserGroupSubsystem @@ -82,11 +83,11 @@ type AllDependencies = Error UserGroupSubsystemError ] -runDependenciesFailOnError :: (HasCallStack) => [User] -> Map TeamId [TeamMember] -> Sem AllDependencies (IO ()) -> IO () -runDependenciesFailOnError usrs team = either (error . ("no assertion: " <>) . show) id . runDependencies usrs team +runDependenciesFailOnError :: (HasCallStack) => [StoredUser] -> Map TeamId [TeamMember] -> Sem AllDependencies (IO ()) -> IO () +runDependenciesFailOnError usrs team = either (error . ("no assertion: " <>) . show) Imports.id . runDependencies usrs team runDependencies :: - [User] -> + [StoredUser] -> Map TeamId [TeamMember] -> Sem AllDependencies a -> Either UserGroupSubsystemError a @@ -95,7 +96,7 @@ runDependencies initialUsers initialTeams = interpretDependencies :: forall r a. - [User] -> + [StoredUser] -> Map TeamId [TeamMember] -> Sem (AllDependencies `Append` r) a -> Sem ('[Error UserGroupSubsystemError] `Append` r) a @@ -109,10 +110,10 @@ interpretDependencies initialUsers initialTeams = . runInMemoryUserGroupStore def . miniGalleyAPIAccess initialTeams def . interpretTeamSubsystemToGalleyAPI - . userSubsystemTestInterpreter initialUsers + . runInMemoryUserSubsytemInterpreter initialUsers mempty runDependenciesWithReturnState :: - [User] -> + [StoredUser] -> Map TeamId [TeamMember] -> Sem AllDependencies a -> Either UserGroupSubsystemError ([Push], a) @@ -128,7 +129,7 @@ runDependenciesWithReturnState initialUsers initialTeams = . runInMemoryUserGroupStore def . miniGalleyAPIAccess initialTeams def . interpretTeamSubsystemToGalleyAPI - . userSubsystemTestInterpreter initialUsers + . runInMemoryUserSubsytemInterpreter initialUsers mempty expectRight :: (Show err) => Either err Property -> Property expectRight = \case @@ -164,7 +165,7 @@ spec = timeoutHook $ describe "UserGroupSubsystem.Interpreter" do runDependenciesWithReturnState (allUsers team) (galleyTeam team) . interpretUserGroupSubsystem $ do - let newUserGroup' = (newUserGroup newUserGroupName) {members = User.userId <$> V.fromList members} :: NewUserGroup + let newUserGroup' = (newUserGroup newUserGroupName) {members = (.id) <$> V.fromList members} :: NewUserGroup createdGroup <- createGroup (ownerId team) newUserGroup' retrievedGroup <- getGroup (ownerId team) createdGroup.id_ False now <- toUTCTimeMillis <$> get @@ -234,19 +235,19 @@ spec = timeoutHook $ describe "UserGroupSubsystem.Interpreter" do . runDependencies (allUsers team) (galleyTeam team) . interpretUserGroupSubsystem $ do - let newUserGroup' = (newUserGroup newUserGroupName) {members = User.userId <$> V.fromList (allUsers team)} :: NewUserGroup + let newUserGroup' = (newUserGroup newUserGroupName) {members = (.id) <$> V.fromList (allUsers team)} :: NewUserGroup [nonAdminUser] = someAdminsOrOwners 1 team - void $ createGroup (User.userId nonAdminUser) newUserGroup' + void $ createGroup (nonAdminUser.id) newUserGroup' unexpected prop "only team members are allowed in the group" $ \team otherUsers newUserGroupName -> - let othersWithoutTeamMembers = filter (\u -> u.userTeam /= Just team.tid) otherUsers + let othersWithoutTeamMembers = filter (\u -> u.teamId /= Just team.tid) otherUsers in notNull othersWithoutTeamMembers ==> expectLeft UserGroupMemberIsNotInTheSameTeam . runDependencies (allUsers team <> otherUsers) (galleyTeam team) . interpretUserGroupSubsystem $ do - let newUserGroup' = (newUserGroup newUserGroupName) {members = User.userId <$> V.fromList otherUsers} :: NewUserGroup + let newUserGroup' = (newUserGroup newUserGroupName) {members = (.id) <$> V.fromList otherUsers} :: NewUserGroup void $ createGroup (ownerId team) newUserGroup' unexpected @@ -325,7 +326,7 @@ spec = timeoutHook $ describe "UserGroupSubsystem.Interpreter" do it "getGroups: q=, returning 0, 1, 2 groups" $ do WithMods team1 :: WithMods '[AtLeastOneNonAdmin] ArbitraryTeam <- generate arbitrary runDependenciesFailOnError (allUsers team1) (galleyTeam team1) . interpretUserGroupSubsystem $ do - let newGroups = [newUserGroup (either undefined id $ userGroupNameFromText name) | name <- ["1", "2", "2", "33"]] + let newGroups = [newUserGroup (either undefined Imports.id $ userGroupNameFromText name) | name <- ["1", "2", "2", "33"]] groups <- (\ng -> passTime 1 >> createGroup (ownerId team1) ng) `mapM` newGroups get0 <- getGroups (ownerId team1) def {searchString = Just "nope"} @@ -347,7 +348,7 @@ spec = timeoutHook $ describe "UserGroupSubsystem.Interpreter" do . runDependencies (allUsers team1) (galleyTeam team1) . interpretUserGroupSubsystem $ do - let mkNewGroup = newUserGroup (either undefined id $ userGroupNameFromText "same name") + let mkNewGroup = newUserGroup (either undefined Imports.id $ userGroupNameFromText "same name") mkGroup = passTime 1 >> createGroup (ownerId team1) mkNewGroup -- groups are only distinguished by creation date @@ -435,7 +436,7 @@ spec = timeoutHook $ describe "UserGroupSubsystem.Interpreter" do it "getGroups (ordering)" $ do WithMods team1 :: WithMods '[AtLeastOneNonAdmin] ArbitraryTeam <- generate arbitrary runDependenciesFailOnError (allUsers team1) (galleyTeam team1) . interpretUserGroupSubsystem $ do - let mkGroup name = createGroup (ownerId team1) (newUserGroup $ either undefined id $ userGroupNameFromText name) + let mkGroup name = createGroup (ownerId team1) (newUserGroup $ either undefined Imports.id $ userGroupNameFromText name) -- construct groups such that there are groups with same name and different creation -- date and vice versa. create names in random order (not alpha). the digits are @@ -538,10 +539,10 @@ spec = timeoutHook $ describe "UserGroupSubsystem.Interpreter" do . runDependencies (allUsers team) (galleyTeam team) . interpretUserGroupSubsystem $ do - let newUserGroup' = (newUserGroup newUserGroupName) {members = User.userId <$> V.fromList (allUsers team)} :: NewUserGroup + let newUserGroup' = (newUserGroup newUserGroupName) {members = (.id) <$> V.fromList (allUsers team)} :: NewUserGroup [nonAdminUser] = someAdminsOrOwners 1 team grp <- createGroup (ownerId team) newUserGroup' - void $ updateGroup (User.userId nonAdminUser) grp.id_ (UserGroupUpdate newUserGroupName2) + void $ updateGroup nonAdminUser.id grp.id_ (UserGroupUpdate newUserGroupName2) unexpected describe "DeleteGroup :: UserId -> UserGroupId -> UserGroupSubsystem m ()" $ do @@ -608,7 +609,7 @@ spec = timeoutHook $ describe "UserGroupSubsystem.Interpreter" do $ do grp <- createGroup (ownerId team) (newUserGroup groupName) let [nonAdminUser] = someAdminsOrOwners 1 team - void $ deleteGroup (User.userId nonAdminUser) grp.id_ + void $ deleteGroup nonAdminUser.id grp.id_ unexpected describe "AddUser, RemoveUser :: UserId -> UserGroupId -> UserId -> UserGroupSubsystem m ()" $ do @@ -621,23 +622,23 @@ spec = timeoutHook $ describe "UserGroupSubsystem.Interpreter" do $ do ug :: UserGroup <- createGroup (ownerId team) (newUserGroup newGroupName) - addUser (ownerId team) ug.id_ (User.userId mbr1) + addUser (ownerId team) ug.id_ mbr1.id ugWithFirst <- getGroup (ownerId team) ug.id_ False - addUser (ownerId team) ug.id_ (User.userId mbr1) + addUser (ownerId team) ug.id_ mbr1.id ugWithIdemP <- getGroup (ownerId team) ug.id_ False - addUser (ownerId team) ug.id_ (User.userId mbr2) + addUser (ownerId team) ug.id_ mbr2.id ugWithSecond <- getGroup (ownerId team) ug.id_ False - removeUser (ownerId team) ug.id_ (User.userId mbr1) + removeUser (ownerId team) ug.id_ mbr1.id ugWithoutFirst <- getGroup (ownerId team) ug.id_ False - removeUser (ownerId team) ug.id_ (User.userId mbr1) -- idemp + removeUser (ownerId team) ug.id_ mbr1.id -- idemp let propertyCheck = - ((.members) <$> ugWithFirst) === Just (Identity $ V.fromList [User.userId mbr1]) - .&&. ((.members) <$> ugWithIdemP) === Just (Identity $ V.fromList [User.userId mbr1]) - .&&. ((sort . V.toList . runIdentity . (.members)) <$> ugWithSecond) === Just (sort [User.userId mbr1, User.userId mbr2]) - .&&. ((.members) <$> ugWithoutFirst) === Just (Identity $ V.fromList [User.userId mbr2]) + ((.members) <$> ugWithFirst) === Just (Identity $ V.fromList [mbr1.id]) + .&&. ((.members) <$> ugWithIdemP) === Just (Identity $ V.fromList [mbr1.id]) + .&&. ((sort . V.toList . runIdentity . (.members)) <$> ugWithSecond) === Just (sort [mbr1.id, mbr2.id]) + .&&. ((.members) <$> ugWithoutFirst) === Just (Identity $ V.fromList [mbr2.id]) pure (ug, propertyCheck) assertUpdateEvent :: UserGroup -> Push -> Property @@ -646,7 +647,7 @@ spec = timeoutHook $ describe "UserGroupSubsystem.Interpreter" do push.origin === Just (ownerId team) .&&. ugid === ug.id_ .&&. Set.fromList push.recipients - === Set.fromList [Recipient {recipientUserId = User.userId user, recipientClients = RecipientClientsAll} | user <- allAdmins team] + === Set.fromList [Recipient {recipientUserId = user.id, recipientClients = RecipientClientsAll} | user <- allAdmins team] _ -> counterexample ("Failed to decode push: " <> show push) False in case resultOrError of Left err -> counterexample ("Unexpected error: " <> show err) False @@ -728,24 +729,24 @@ instance (ArbitraryWithMods mods a) => Arbitrary (WithMods mods a) where data ArbitraryTeam = ArbitraryTeam { tid :: TeamId, - owner :: (User, TeamMember), - members :: [(User, TeamMember)] + owner :: (StoredUser, TeamMember), + members :: [(StoredUser, TeamMember)] } deriving (Show, Eq) instance Arbitrary ArbitraryTeam where arbitrary = do tid <- arbitrary - let assignTeam u = u {userTeam = Just tid} + let assignTeam u = u {teamId = Just tid} :: StoredUser adminUser <- assignTeam <$> arbitrary adminMember <- arbitrary @TeamMember <&> (permissions .~ rolePermissions RoleOwner) - <&> (TM.userId .~ User.userId adminUser) + <&> (TM.userId .~ adminUser.id) otherUsers <- listOf' arbitrary otherUserWithMembers <- for otherUsers $ \u -> do mem <- arbitrary - pure (u, mem & TM.userId .~ User.userId u) + pure (u, mem & TM.userId .~ u.id) pure . ArbitraryTeam tid (adminUser, adminMember) $ map (first assignTeam) otherUserWithMembers shrink team = @@ -755,13 +756,13 @@ instance Arbitrary ArbitraryTeam where let lessMembers = take (length team.members `div` 2) team.members in [team {members = lessMembers}] -allUsers :: ArbitraryTeam -> [User] +allUsers :: ArbitraryTeam -> [StoredUser] allUsers t = fst <$> t.owner : t.members ownerId :: ArbitraryTeam -> UserId -ownerId t = User.userId (fst t.owner) +ownerId t = (fst t.owner).id -allAdmins :: ArbitraryTeam -> [User] +allAdmins :: ArbitraryTeam -> [StoredUser] allAdmins t = fst <$> filter (isAdminOrOwner . (^. permissions) . snd) (t.owner : t.members) -- | The Map is required by the mock GalleyAPIAccess @@ -771,10 +772,10 @@ galleyTeam t = galleyTeamWithExtra t [] galleyTeamWithExtra :: ArbitraryTeam -> [TeamMember] -> Map TeamId [TeamMember] galleyTeamWithExtra t tm = Map.singleton t.tid $ tm <> map snd (t.owner : t.members) -someAdminsOrOwners :: Int -> ArbitraryTeam -> [User] +someAdminsOrOwners :: Int -> ArbitraryTeam -> [StoredUser] someAdminsOrOwners num team = someMembersWithRoles num team (Just [RoleMember, RoleExternalPartner]) -someMembersWithRoles :: (HasCallStack) => Int -> ArbitraryTeam -> Maybe [Role] -> [User] +someMembersWithRoles :: (HasCallStack) => Int -> ArbitraryTeam -> Maybe [Role] -> [StoredUser] someMembersWithRoles num team mbRoles = result where result = diff --git a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs index f5e233fd454..80f64d8a955 100644 --- a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs @@ -59,6 +59,7 @@ import Wire.API.User hiding (DeleteUser) import Wire.API.User.IdentityProvider (IdPList (..), team) import Wire.API.User.Search import Wire.API.UserEvent +import Wire.AppSubsystem import Wire.AuthenticationSubsystem.Error import Wire.DomainRegistrationStore qualified as DRS import Wire.IndexedUserStore qualified as IU @@ -104,8 +105,8 @@ spec = describe "UserSubsystem.Interpreter" do mkExpectedProfiles domain users = [ mkUserProfileWithEmail Nothing - (if isJust targetUser.serviceId then UserTypeBot else UserTypeRegular) (mkUserFromStored domain miniLocale targetUser) + Nothing defUserLegalHoldStatus | targetUser <- users ] @@ -129,6 +130,7 @@ spec = describe "UserSubsystem.Interpreter" do result = run . runErrorUnsafe @UserSubsystemError + . runErrorUnsafe @AppSubsystemError . runErrorUnsafe @AuthenticationSubsystemError . runErrorUnsafe @RateLimitExceeded . runErrorUnsafe @TeamCollaboratorsError @@ -164,8 +166,8 @@ spec = describe "UserSubsystem.Interpreter" do in retrievedProfiles === [ mkUserProfile (fmap (const $ (,) <$> viewer.teamId <*> Just teamMember) config.emailVisibilityConfig) - (if isJust targetUser.serviceId then UserTypeBot else UserTypeRegular) (mkUserFromStored domain config.defaultLocale targetUser) + Nothing defUserLegalHoldStatus ] @@ -181,8 +183,8 @@ spec = describe "UserSubsystem.Interpreter" do in retrievedProfile === [ mkUserProfile (fmap (const Nothing) config.emailVisibilityConfig) - (if isJust targetUser.serviceId then UserTypeBot else UserTypeRegular) (mkUserFromStored domain config.defaultLocale targetUser) + Nothing defUserLegalHoldStatus ] @@ -802,9 +804,12 @@ spec = describe "UserSubsystem.Interpreter" do localBackend = def {users = [storedUser]} in updateResult === Left UserSubsystemInvalidHandle - prop "update / read supported-protocols" \(storedUser, config, newSupportedProtocols) -> - not (hasPendingInvitation storedUser) ==> - let luid :: Local UserId + prop "update / read supported-protocols" \(storedUser_, config, newSupportedProtocols) -> + not (hasPendingInvitation storedUser_) ==> + let storedUser :: StoredUser + storedUser = storedUser_ {userType = Just UserTypeRegular} + + luid :: Local UserId luid = toLocalUnsafe dom storedUser.id where dom = Domain "localdomain" @@ -1100,10 +1105,7 @@ spec = describe "UserSubsystem.Interpreter" do searchee = searcheeNoHandle {handle = Just searcheeHandle} :: StoredUser storedUserToDoc :: StoredUser -> UserDoc - storedUserToDoc user = - let indexUser = storedUserToIndexUser user - userType = if isJust user.serviceId then UserTypeBot else UserTypeRegular - in indexUserToDoc defaultSearchVisibilityInbound (Just userType) Nothing indexUser + storedUserToDoc user = indexUserToDoc defaultSearchVisibilityInbound Nothing (storedUserToIndexUser user) indexFromStoredUsers :: [StoredUser] -> UserIndex indexFromStoredUsers storedUsers = do @@ -1133,6 +1135,6 @@ spec = describe "UserSubsystem.Interpreter" do contactName = fromName searchee.name, contactHandle = fromHandle <$> searchee.handle, contactColorId = Just . fromIntegral $ searchee.accentId.fromColourId, - contactType = UserTypeRegular + contactType = fromMaybe UserTypeRegular searchee.userType } pure $ result.searchResults === [expectedContact | fromMaybe True searchee.searchable] diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal index 706fab690e2..87830c3837d 100644 --- a/libs/wire-subsystems/wire-subsystems.cabal +++ b/libs/wire-subsystems/wire-subsystems.cabal @@ -250,9 +250,15 @@ library Wire.ConversationStore.MLS.Types Wire.ConversationStore.Postgres Wire.ConversationSubsystem + Wire.ConversationSubsystem.CreateInternal + Wire.ConversationSubsystem.Fetch + Wire.ConversationSubsystem.Internal Wire.ConversationSubsystem.Interpreter + Wire.ConversationSubsystem.Notify Wire.ConversationSubsystem.One2One Wire.ConversationSubsystem.Util + Wire.CustomBackendStore + Wire.CustomBackendStore.Cassandra Wire.DeleteQueue Wire.DeleteQueue.InMemory Wire.DomainRegistrationStore @@ -315,6 +321,7 @@ library Wire.LegalHoldStore.Cassandra.Queries Wire.LegalHoldStore.Env Wire.ListItems + Wire.ListItems.Team.Cassandra Wire.MeetingsStore Wire.MeetingsStore.Postgres Wire.MeetingsSubsystem @@ -371,6 +378,10 @@ library Wire.TeamInvitationSubsystem.Interpreter Wire.TeamJournal Wire.TeamJournal.Aws + Wire.TeamMemberStore + Wire.TeamMemberStore.Cassandra + Wire.TeamNotificationStore + Wire.TeamNotificationStore.Cassandra Wire.TeamStore Wire.TeamStore.Cassandra Wire.TeamStore.Cassandra.Queries diff --git a/nix/overlay.nix b/nix/overlay.nix index 460e9c0e425..3d458be2076 100644 --- a/nix/overlay.nix +++ b/nix/overlay.nix @@ -32,8 +32,6 @@ 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 // { diff --git a/nix/pkgs/sbomqs/default.nix b/nix/pkgs/sbomqs/default.nix deleted file mode 100644 index d0e41ad5785..00000000000 --- a/nix/pkgs/sbomqs/default.nix +++ /dev/null @@ -1,21 +0,0 @@ -{ buildGoModule, fetchFromGitHub, lib, ... }: -buildGoModule rec { - pname = "sbomqs"; - version = "0.0.30"; - - src = fetchFromGitHub { - owner = "interlynk-io"; - repo = "sbomqs"; - rev = "v${version}"; - hash = "sha256-+y7+xi+E8kjGUjhIRKNk6ogcQMP+Dp39LrL66B1XdrQ="; - }; - - vendorHash = "sha256-V6k7nF2ovyl4ELE8Cqe/xjpmPAKI0t5BNlssf41kd0Y="; - - meta = with lib; { - description = "SBOM quality score - Quality metrics for your sboms"; - homepage = "https://github.com/interlynk-io/sbomqs"; - license = licenses.asl20; - mainProgram = "sbomqs"; - }; -} diff --git a/nix/wire-server.nix b/nix/wire-server.nix index 5345d02c4d2..8c1122d40d0 100644 --- a/nix/wire-server.nix +++ b/nix/wire-server.nix @@ -486,9 +486,53 @@ let localPkgs = map (e: (hPkgs localModsEnableAll).${e}) wireServerPackages; bomDependencies = bomDependenciesDrv pkgs localPkgs haskellPackages; + + devEnvPkgs = commonTools ++ [ + pkgs.bash + pkgs.crate2nix + pkgs.dash + (pkgs.haskell-language-server.override { supportedGhcVersions = [ "910" ]; }) + pkgs.ghcid + pkgs.kind + pkgs.netcat + pkgs.niv + pkgs.haskell.packages.ghc912.apply-refact + (pkgs.python3.withPackages + (ps: with ps; [ + black + bokeh + flake8 + ipdb + ipython + protobuf + pylint + pyyaml + requests + websockets + ])) + pkgs.rsync + pkgs.wget + pkgs.yq + pkgs.nginz + pkgs.rabbitmqadmin + pkgs.postgresql + + pkgs_24_11.cabal-install + pkgs.nix-prefetch-git + pkgs.haskellPackages.cabal-plan + pkgs.lsof + pkgs.haskellPackages.headroom + profileEnv + ] + ++ ghcWithPackages + ++ pkgs.lib.optionals pkgs.stdenv.isLinux [ + # linux-only, not strictly required tools + pkgs.docker-compose + (pkgs.telepresence.override { pythonPackages = pkgs.python310Packages; }) + ]; in { - inherit ciImage hoogleImage allImages haskellPackages haskellPackagesUnoptimizedNoDocs imagesList bomDependencies; + inherit ciImage hoogleImage allImages haskellPackages haskellPackagesUnoptimizedNoDocs imagesList bomDependencies devEnvPkgs; images = images localModsEnableAll; imagesUnoptimizedNoDocs = images localModsOnlyTests; @@ -503,50 +547,7 @@ in devEnv = pkgs.buildEnv { name = "wire-server-dev-env"; ignoreCollisions = true; - paths = commonTools ++ [ - pkgs.bash - pkgs.crate2nix - pkgs.dash - (pkgs.haskell-language-server.override { supportedGhcVersions = [ "910" ]; }) - pkgs.ghcid - pkgs.kind - pkgs.netcat - pkgs.niv - pkgs.haskell.packages.ghc912.apply-refact - (pkgs.python3.withPackages - (ps: with ps; [ - black - bokeh - flake8 - ipdb - ipython - protobuf - pylint - pyyaml - requests - websockets - ])) - pkgs.rsync - pkgs.wget - pkgs.yq - pkgs.nginz - pkgs.rabbitmqadmin - pkgs.sbomqs - pkgs.postgresql - - pkgs_24_11.cabal-install - pkgs.nix-prefetch-git - pkgs.haskellPackages.cabal-plan - pkgs.lsof - pkgs.haskellPackages.headroom - profileEnv - ] - ++ ghcWithPackages - ++ pkgs.lib.optionals pkgs.stdenv.isLinux [ - # linux-only, not strictly required tools - pkgs.docker-compose - (pkgs.telepresence.override { pythonPackages = pkgs.python310Packages; }) - ]; + paths = devEnvPkgs; }; inherit brig-templates; diff --git a/postgres-schema.sql b/postgres-schema.sql index e6d915bae79..070828aa351 100644 --- a/postgres-schema.sql +++ b/postgres-schema.sql @@ -9,8 +9,8 @@ \restrict 79bbfb4630959c48307653a5cd3d83f2582b3c2210f75f10d79e3ebf0015620 --- Dumped from database version 17.6 --- Dumped by pg_dump version 17.6 +-- Dumped from database version 17.7 +-- Dumped by pg_dump version 17.7 SET statement_timeout = 0; SET lock_timeout = 0; @@ -40,6 +40,20 @@ ALTER SCHEMA public OWNER TO "wire-server"; COMMENT ON SCHEMA public IS ''; +-- +-- Name: recurrence_frequency; Type: TYPE; Schema: public; Owner: wire-server +-- + +CREATE TYPE public.recurrence_frequency AS ENUM ( + 'daily', + 'weekly', + 'monthly', + 'yearly' +); + + +ALTER TYPE public.recurrence_frequency OWNER TO "wire-server"; + SET default_tablespace = ''; SET default_table_access_method = heap; @@ -96,7 +110,8 @@ CREATE TABLE public.conversation ( receipt_mode integer, team uuid, type integer NOT NULL, - parent_conv uuid + parent_conv uuid, + history_depth bigint ); @@ -177,33 +192,27 @@ CREATE TABLE public.local_conversation_remote_member ( ALTER TABLE public.local_conversation_remote_member OWNER TO "wire-server"; --- --- Name: meetings; Type: ENUM; Schema: public; Owner: wire-server --- - -CREATE TYPE recurrence_frequency AS ENUM ('daily', 'weekly', 'monthly', 'yearly'); - - -ALTER TABLE public.recurrence_frequency OWNER TO "wire-server"; - -- -- Name: meetings; Type: TABLE; Schema: public; Owner: wire-server -- CREATE TABLE public.meetings ( - id uuid NOT NULL DEFAULT gen_random_uuid(), + id uuid DEFAULT gen_random_uuid() NOT NULL, title text NOT NULL, creator uuid NOT NULL, - start_time timestamptz NOT NULL, - end_time timestamptz NOT NULL, - recurrence_frequency recurrence_frequency, + start_time timestamp with time zone NOT NULL, + end_time timestamp with time zone NOT NULL, + recurrence_frequency public.recurrence_frequency, recurrence_interval integer, - recurrence_until timestamptz, + recurrence_until timestamp with time zone, conversation_id uuid NOT NULL, - invited_emails text[] DEFAULT '{}'::text[], - trial boolean DEFAULT false, - created_at timestamp with time zone DEFAULT now(), - updated_at timestamp with time zone DEFAULT now() + invited_emails text[] DEFAULT '{}'::text[] NOT NULL, + trial boolean DEFAULT false NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT meetings_title_length CHECK ((length(title) <= 256)), + CONSTRAINT meetings_title_not_empty CHECK ((length(TRIM(BOTH FROM title)) > 0)), + CONSTRAINT meetings_valid_time_range CHECK ((end_time > start_time)) ); @@ -385,19 +394,19 @@ ALTER TABLE ONLY public.conversation -- --- Name: meetings meetings_pkey; Type: CONSTRAINT; Schema: public; Owner: wire-server +-- Name: local_conversation_remote_member local_conversation_remote_member_pkey; Type: CONSTRAINT; Schema: public; Owner: wire-server -- -ALTER TABLE ONLY public.meetings - ADD CONSTRAINT meetings_pkey PRIMARY KEY (id); +ALTER TABLE ONLY public.local_conversation_remote_member + ADD CONSTRAINT local_conversation_remote_member_pkey PRIMARY KEY (conv, user_remote_domain, user_remote_id); -- --- Name: local_conversation_remote_member local_conversation_remote_member_pkey; Type: CONSTRAINT; Schema: public; Owner: wire-server +-- Name: meetings meetings_pkey; Type: CONSTRAINT; Schema: public; Owner: wire-server -- -ALTER TABLE ONLY public.local_conversation_remote_member - ADD CONSTRAINT local_conversation_remote_member_pkey PRIMARY KEY (conv, user_remote_domain, user_remote_id); +ALTER TABLE ONLY public.meetings + ADD CONSTRAINT meetings_pkey PRIMARY KEY (id); -- diff --git a/renovate.json b/renovate.json index d39df4e07d2..d993844ca33 100644 --- a/renovate.json +++ b/renovate.json @@ -40,5 +40,11 @@ "vulnerabilityAlerts": { "enabled": true }, - "osvVulnerabilityAlerts": true + "osvVulnerabilityAlerts": true, + "nix": { + "enabled": true + }, + "lockFileMaintenance": { + "enabled": true + } } diff --git a/services/background-worker/shutdown_test.sh b/services/background-worker/shutdown_test.sh index ea0797b892e..64a0477c286 100755 --- a/services/background-worker/shutdown_test.sh +++ b/services/background-worker/shutdown_test.sh @@ -36,7 +36,7 @@ progress-bar 30 echo "" echo "Sending SIGKILL" pkill -SIGKILL background-work -if [ $? -eq 1 ] +if [[ $? -eq 1 ]] then echo "Graceful shutdown, background-worker had already shutdown" else diff --git a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs index ee2ab48773b..d9ae7db190f 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs @@ -95,7 +95,6 @@ import Wire.TeamStore.Cassandra (interpretTeamStoreToCassandra) import Wire.TeamSubsystem.Interpreter (TeamSubsystemConfig (..), interpretTeamSubsystem) import Wire.UserClientIndexStore.Cassandra import Wire.UserGroupStore.Postgres (interpretUserGroupStoreToPostgres) -import Wire.UserStore.Cassandra (interpretUserStoreCassandra) -- Helper functions for LegalHoldEnv -- Adapted from Galley.External.LegalHoldService.Internal @@ -213,7 +212,6 @@ dispatchJob job = do . runInputConst legalHoldEnv . runInputConst (ExposeInvitationURLsAllowlist []) . interpretServiceStoreToCassandra env.cassandraBrig - . interpretUserStoreCassandra env.cassandraBrig . interpretUserGroupStoreToPostgres . interpretTeamFeatureStoreToCassandra . interpretUserClientIndexStoreToCassandra env.cassandraGalley diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 3e740243f34..5950ef6270e 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -350,7 +350,6 @@ executable brig-integration other-modules: API.Calling API.Federation - API.Internal API.Metrics API.MLS.Util API.OAuth @@ -371,7 +370,6 @@ executable brig-integration API.User.Handles API.User.RichInfo API.User.Util - API.UserPendingActivation Federation.End2end Federation.Util Index.Create diff --git a/services/brig/brig.integration.yaml b/services/brig/brig.integration.yaml index 7d39b7166de..fc23b069f74 100644 --- a/services/brig/brig.integration.yaml +++ b/services/brig/brig.integration.yaml @@ -267,6 +267,7 @@ optSettings: maxRateLimitedKeys: 100000 # Estimated memory usage: 4 MB setChallengeTTL: 172800 setEphemeralUserCreationEnabled: true + setConsumableNotifications: false logLevel: Warn logNetStrings: false diff --git a/services/brig/docs/swagger-v10.json b/services/brig/docs/swagger-v10.json index 49b0f5dcd90..186e7cea041 100644 --- a/services/brig/docs/swagger-v10.json +++ b/services/brig/docs/swagger-v10.json @@ -11105,7 +11105,7 @@ }, "/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.", + "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 in the JSON response body as a JWT DPoP token.", "operationId": "create-access-token", "parameters": [ { diff --git a/services/brig/docs/swagger-v11.json b/services/brig/docs/swagger-v11.json index 0f3d7def253..30b099c6f37 100644 --- a/services/brig/docs/swagger-v11.json +++ b/services/brig/docs/swagger-v11.json @@ -11222,7 +11222,7 @@ }, "/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.", + "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 in the JSON response body as a JWT DPoP token.", "operationId": "create-access-token", "parameters": [ { diff --git a/services/brig/docs/swagger-v12.json b/services/brig/docs/swagger-v12.json index a2ec7757211..f987694b490 100644 --- a/services/brig/docs/swagger-v12.json +++ b/services/brig/docs/swagger-v12.json @@ -11550,7 +11550,7 @@ }, "/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.", + "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 in the JSON response body as a JWT DPoP token.", "operationId": "create-access-token", "parameters": [ { diff --git a/services/brig/docs/swagger-v13.json b/services/brig/docs/swagger-v13.json index 1dacbe6236e..0243c80c14a 100644 --- a/services/brig/docs/swagger-v13.json +++ b/services/brig/docs/swagger-v13.json @@ -11597,7 +11597,7 @@ }, "/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.", + "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 in the JSON response body as a JWT DPoP token.", "operationId": "create-access-token", "parameters": [ { diff --git a/services/brig/docs/swagger-v14.json b/services/brig/docs/swagger-v14.json index c8a6bd595e5..0916d2a95a7 100644 --- a/services/brig/docs/swagger-v14.json +++ b/services/brig/docs/swagger-v14.json @@ -12113,7 +12113,7 @@ }, "/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.", + "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 in the JSON response body as a JWT DPoP token.", "operationId": "create-access-token", "parameters": [ { diff --git a/services/brig/docs/swagger-v15.json b/services/brig/docs/swagger-v15.json new file mode 100644 index 00000000000..4ec4c405de1 --- /dev/null +++ b/services/brig/docs/swagger-v15.json @@ -0,0 +1 @@ +{"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/RequireExternalEmailVerificationConfig.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"},"AppInfo":{"properties":{"category":{"description":"Category name (if uncertain, pick \"other\")","type":"string"},"description":{"maxLength":300,"minLength":0,"type":"string"}},"required":["category","description"],"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":{"$ref":"#/components/schemas/URI"},"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"},"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/UncheckedPrekeyBundle"}},"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"},"type":{"$ref":"#/components/schemas/UserType"}},"required":["qualified_id","name","type"],"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"},"history":{"$ref":"#/components/schemas/History"},"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"},"ConversationHistoryUpdate":{"properties":{"history":{"$ref":"#/components/schemas/History"}},"required":["history"],"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"},"history":{"$ref":"#/components/schemas/History"},"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/UserProfile"}},"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"},"depth":{"$ref":"#/components/schemas/HistoryDuration"},"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"},"history":{"$ref":"#/components/schemas/History"},"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","depth"],"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","conversation.history-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"},"Frequency":{"enum":["daily","weekly","monthly","yearly"],"type":"string"},"GetByEmailReq":{"properties":{"email":{"$ref":"#/components/schemas/Email"}},"required":["email"],"type":"object"},"GetByEmailResp":{"properties":{"sso_code":{"$ref":"#/components/schemas/UUID"}},"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","meeting"],"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"},"History":{"properties":{"depth":{"$ref":"#/components/schemas/HistoryDuration"}},"required":["depth"],"type":"object"},"HistoryDuration":{"type":"string"},"HistorySharingConfig":{"properties":{"depth":{"$ref":"#/components/schemas/HistoryDuration"}},"required":["depth"],"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":{"properties":{"extraInfo":{"$ref":"#/components/schemas/WireIdP"},"id":{"$ref":"#/components/schemas/URI"},"metadata":{"$ref":"#/components/schemas/IdPMetadata"}},"required":["id","metadata","extraInfo"],"type":"object"},"IdPList":{"properties":{"providers":{"items":{"$ref":"#/components/schemas/IdPConfig"},"type":"array"}},"required":["providers"],"type":"object"},"IdPMetadata":{"properties":{"certAuthnResponse":{"items":{"$ref":"#/components/schemas/SignedCertificate"},"minItems":1,"type":"array"},"issuer":{"$ref":"#/components/schemas/URI"},"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"},"Meeting":{"description":"A scheduled meeting","properties":{"created_at":{"$ref":"#/components/schemas/UTCTime"},"end_time":{"$ref":"#/components/schemas/UTCTime"},"invited_emails":{"items":{"$ref":"#/components/schemas/Email"},"type":"array"},"qualified_conversation":{"$ref":"#/components/schemas/Qualified_ConvId"},"qualified_creator":{"$ref":"#/components/schemas/Qualified_UserId"},"qualified_id":{"$ref":"#/components/schemas/Qualified_MeetingId"},"recurrence":{"$ref":"#/components/schemas/Recurrence"},"start_time":{"$ref":"#/components/schemas/UTCTime"},"title":{"maxLength":256,"minLength":1,"type":"string"},"trial":{"type":"boolean"},"updated_at":{"$ref":"#/components/schemas/UTCTime"}},"required":["qualified_id","title","qualified_creator","start_time","end_time","qualified_conversation","invited_emails","trial","created_at","updated_at"],"type":"object"},"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":{"accent_id":{"format":"int32","maximum":2147483647,"minimum":-2147483648,"type":"integer"},"assets":{"items":{"$ref":"#/components/schemas/UserAsset"},"type":"array"},"category":{"description":"Category name (if uncertain, pick \"other\")","type":"string"},"description":{"maxLength":300,"minLength":0,"type":"string"},"name":{"maxLength":128,"minLength":1,"type":"string"},"password":{"maxLength":1024,"minLength":6,"type":"string"}},"required":["name","category","description","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/UncheckedPrekeyBundle"},"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/UncheckedPrekeyBundle"},"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"},"history":{"$ref":"#/components/schemas/History"},"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"},"NewMeeting":{"description":"Request to create a new meeting","properties":{"end_time":{"$ref":"#/components/schemas/UTCTime"},"invited_emails":{"items":{"$ref":"#/components/schemas/Email"},"type":"array"},"recurrence":{"$ref":"#/components/schemas/Recurrence"},"start_time":{"$ref":"#/components/schemas/UTCTime"},"title":{"maxLength":256,"minLength":1,"type":"string"}},"required":["start_time","end_time","title"],"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_DEPRECATED_USE_ASSETS_INSTEAD"},"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"},"history":{"$ref":"#/components/schemas/History"},"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"},"history":{"$ref":"#/components/schemas/History"},"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"},"history":{"$ref":"#/components/schemas/History"},"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"},"history":{"$ref":"#/components/schemas/History"},"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"},"history":{"$ref":"#/components/schemas/History"},"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_DEPRECATED_USE_ASSETS_INSTEAD":{"items":{"type":"object"},"maxItems":10,"minItems":0,"type":"array"},"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"},"PutApp":{"properties":{"accent_id":{"format":"int32","maximum":2147483647,"minimum":-2147483648,"type":"integer"},"assets":{"items":{"$ref":"#/components/schemas/UserAsset"},"type":"array"},"category":{"description":"Category name (if uncertain, pick \"other\")","type":"string"},"description":{"maxLength":300,"minLength":0,"type":"string"},"name":{"maxLength":128,"minLength":1,"type":"string"}},"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_MeetingId":{"properties":{"domain":{"$ref":"#/components/schemas/Domain"},"id":{"$ref":"#/components/schemas/UUID"}},"required":["domain","id"],"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"},"Recurrence":{"description":"Recurrence pattern for meetings","properties":{"frequency":{"$ref":"#/components/schemas/Frequency"},"interval":{"maximum":9223372036854775807,"minimum":-9223372036854775808,"type":"integer"},"until":{"$ref":"#/components/schemas/UTCTime"}},"required":["frequency"],"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"},"RequireExternalEmailVerificationConfig.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"},"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"},"SignedCertificate":{"type":"string"},"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/URI"}},"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":{"nomadProfiles":{"description":"Whether Nomad client profiles are enabled; null or absence means not enabled.","type":"boolean"},"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":{"nomadProfiles":{"description":"Whether Nomad client profiles are enabled; null or absence means not enabled.","type":"boolean"},"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"},"type":{"$ref":"#/components/schemas/UserType"},"user_groups":{"description":"List of user group ids the user is a member of","items":{"$ref":"#/components/schemas/UUID"},"type":"array"}},"required":["id","type","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"},"URI":{"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"},"UncheckedPrekeyBundle":{"properties":{"id":{"maximum":65535,"minimum":0,"type":"integer"},"key":{"type":"string"}},"required":["id","key"],"type":"object"},"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/UncheckedPrekeyBundle"},"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/UncheckedPrekeyBundle"},"mls_public_keys":{"$ref":"#/components/schemas/MLSPublicKeys"},"prekeys":{"description":"New prekeys for other clients to establish OTR sessions.","items":{"$ref":"#/components/schemas/UncheckedPrekeyBundle"},"type":"array"}},"type":"object"},"UpdateMeeting":{"description":"Request to update a meeting","properties":{"end_time":{"$ref":"#/components/schemas/UTCTime"},"recurrence":{"$ref":"#/components/schemas/Recurrence"},"start_time":{"$ref":"#/components/schemas/UTCTime"},"title":{"maxLength":256,"minLength":1,"type":"string"}},"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_DEPRECATED_USE_ASSETS_INSTEAD"},"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"},"type":{"$ref":"#/components/schemas/UserType"}},"required":["qualified_id","type","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/UncheckedPrekeyBundle"},"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"},"app":{"$ref":"#/components/schemas/AppInfo"},"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_DEPRECATED_USE_ASSETS_INSTEAD"},"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_DEPRECATED_USE_ASSETS_INSTEAD"},"text_status":{"maxLength":256,"minLength":1,"type":"string"}},"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":[15],"domain":"example.com","federation":false,"supported":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]},"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,15],"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":{"enum":["WireIdPAPIV1","WireIdPAPIV2"],"type":"string"},"domain":{"type":"string"},"handle":{"type":"string"},"oldIssuers":{"items":{"$ref":"#/components/schemas/URI"},"type":"array"},"replacedBy":{"type":"string"},"team":{"$ref":"#/components/schemas/UUID"}},"required":["team","apiVersion","oldIssuers","replacedBy","handle","domain"],"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 in the JSON response body as a JWT DPoP token.","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":"mls-duplicate-public-key","message":"MLS public key for the given signature scheme already exists"},"properties":{"code":{"enum":[400],"type":"integer"},"label":{"enum":["mls-duplicate-public-key","bad-request"],"type":"string"},"message":{"type":"string"}},"required":["code","label","message"],"type":"object"}}},"description":"Invalid `body`\n\nMLS public key for the given signature scheme already exists (label: `mls-duplicate-public-key`)\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":"history-not-supported","message":"Shared history is not supported on this conversation"},"properties":{"code":{"enum":[400],"type":"integer"},"label":{"enum":["history-not-supported","mls-not-enabled","non-empty-member-list"],"type":"string"},"message":{"type":"string"}},"required":["code","label","message"],"type":"object"}}},"description":"Invalid `body`\n\nShared history is not supported on this conversation (label: `history-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`)\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}/history":{"put":{"description":" [internal route ID: \"update-conversation-history\"]\n\n","operationId":"update-conversation-history","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/ConversationHistoryUpdate"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Event"}},"application/json;charset=utf-8":{"schema":{"$ref":"#/components/schemas/Event"}}},"description":"History updated"},"204":{"description":"History unchanged"},"400":{"content":{"application/json;charset=utf-8":{"schema":{"example":{"code":400,"label":"history-not-supported","message":"Shared history is not supported on this conversation"},"properties":{"code":{"enum":[400],"type":"integer"},"label":{"enum":["history-not-supported"],"type":"string"},"message":{"type":"string"}},"required":["code","label","message"],"type":"object"}}},"description":"Invalid `body`\n\nShared history is not supported on this conversation (label: `history-not-supported`)"},"403":{"content":{"application/json;charset=utf-8":{"schema":{"example":{"code":403,"label":"action-denied","message":"Insufficient authorization (missing modify_conversation_access)"},"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 modify_conversation_access) (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":"`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)"}},"summary":"Update history settings of a conversation"}},"/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"}}},"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"}}},"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"}}},"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"}},"/meetings":{"post":{"description":" [internal route ID: \"create-meeting\"]\n\n","operationId":"create-meeting","requestBody":{"content":{"application/json;charset=utf-8":{"schema":{"$ref":"#/components/schemas/NewMeeting"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Meeting"}},"application/json;charset=utf-8":{"schema":{"$ref":"#/components/schemas/Meeting"}}},"description":"Meeting created"},"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"],"type":"string"},"message":{"type":"string"}},"required":["code","label","message"],"type":"object"}}},"description":"Invalid operation (label: `invalid-op`)"},"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 meeting"}},"/meetings/{domain}/{id}":{"get":{"description":" [internal route ID: \"get-meeting\"]\n\n","operationId":"get-meeting","parameters":[{"in":"path","name":"domain","required":true,"schema":{"type":"string"}},{"in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json;charset=utf-8":{"schema":{"$ref":"#/components/schemas/Meeting"}}},"description":""},"404":{"content":{"application/json;charset=utf-8":{"schema":{"example":{"code":404,"label":"meeting-not-found","message":"Meeting not found"},"properties":{"code":{"enum":[404],"type":"integer"},"label":{"enum":["meeting-not-found"],"type":"string"},"message":{"type":"string"}},"required":["code","label","message"],"type":"object"}}},"description":"`domain` or `id` or Meeting not found (label: `meeting-not-found`)"}},"summary":"Get a single meeting by ID"},"put":{"description":" [internal route ID: \"update-meeting\"]\n\n","operationId":"update-meeting","parameters":[{"in":"path","name":"domain","required":true,"schema":{"type":"string"}},{"in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"requestBody":{"content":{"application/json;charset=utf-8":{"schema":{"$ref":"#/components/schemas/UpdateMeeting"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Meeting"}},"application/json;charset=utf-8":{"schema":{"$ref":"#/components/schemas/Meeting"}}},"description":"Meeting 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","access-denied"],"type":"string"},"message":{"type":"string"}},"required":["code","label","message"],"type":"object"}}},"description":"Invalid operation (label: `invalid-op`)\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":"meeting-not-found","message":"Meeting not found"},"properties":{"code":{"enum":[404],"type":"integer"},"label":{"enum":["meeting-not-found"],"type":"string"},"message":{"type":"string"}},"required":["code","label","message"],"type":"object"}}},"description":"`domain` or `id` or Meeting not found (label: `meeting-not-found`)"}},"summary":"Update an existing meeting"}},"/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\nOptional user-type filter semantics: omitted or empty (type=) means no filtering.","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"}},{"description":"Only user types. Omitted or empty (type=) means no filtering.","in":"query","name":"type","required":false,"schema":{"type":"string"}}],"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/get-by-email":{"post":{"description":" [internal route ID: \"sso-get-by-email\"]\n\n","operationId":"sso-get-by-email","requestBody":{"content":{"application/json;charset=utf-8":{"schema":{"$ref":"#/components/schemas/GetByEmailReq"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetByEmailResp"}},"application/json;charset=utf-8":{"schema":{"$ref":"#/components/schemas/GetByEmailResp"}}},"description":"SSO code found"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetByEmailResp"}},"application/json;charset=utf-8":{"schema":{"$ref":"#/components/schemas/GetByEmailResp"}}},"description":"SSO code not found or feature disabled"}}}},"/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":"query","name":"label","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":"query","name":"label","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":{"get":{"description":" [internal route ID: \"get-apps\"]\n\n","operationId":"get-apps","parameters":[{"in":"path","name":"tid","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json;charset=utf-8":{"schema":{"items":{"$ref":"#/components/schemas/UserProfile"},"type":"array"}}},"description":""}},"summary":"Get all apps owned by the given team (not including collaborators)"},"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}":{"put":{"description":" [internal route ID: \"put-app\"]\n\n","operationId":"put-app","parameters":[{"in":"path","name":"tid","required":true,"schema":{"format":"uuid","type":"string"}},{"in":"path","name":"app","required":true,"schema":{"format":"uuid","type":"string"}}],"requestBody":{"content":{"application/json;charset=utf-8":{"schema":{"$ref":"#/components/schemas/PutApp"}}},"required":true},"responses":{"200":{"content":{"application/json;charset=utf-8":{"schema":{"example":[],"items":{},"maxItems":0,"type":"array"}}},"description":""}},"summary":"Update metadata of an existing 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/UserProfile"}}},"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\", RequireExternalEmailVerificationConfig)]\n\n

Controls whether externally managed email addresses (from SAML or SCIM) must be verified by the user, or are auto-activated.

The external feature name is kept as validateSAMLemails for backward compatibility. That name is misleading because the feature also applies to SCIM-managed users, and it controls email ownership verification rather than generic email validation.

","operationId":"get_RequireExternalEmailVerificationConfig","parameters":[{"in":"path","name":"tid","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json;charset=utf-8":{"schema":{"$ref":"#/components/schemas/RequireExternalEmailVerificationConfig.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."}},"/websocket":{"get":{"description":" [internal route ID: \"websocket\"]\n\nThis is a temporary copy of await, please do not use it","externalDocs":{"description":"RFC 6455","url":"https://datatracker.ietf.org/doc/html/rfc6455"},"operationId":"websocket","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"}}},"security":[{"ZAuth":[]}],"servers":[{"url":"/v15"}]} \ No newline at end of file diff --git a/services/brig/docs/swagger-v3.json b/services/brig/docs/swagger-v3.json index 144cd713f9c..f4435f62a46 100644 --- a/services/brig/docs/swagger-v3.json +++ b/services/brig/docs/swagger-v3.json @@ -6831,7 +6831,7 @@ }, "/clients/{cid}/access-token": { "post": { - "description": "[implementation stub, not supported yet!] Create an JWT DPoP access token for the client CSR, given a JWT DPoP proof, specified in the `DPoP` header. The access token will be returned as JWT DPoP token in the `DPoP` header.", + "description": "[implementation stub, not supported yet!] Create an JWT DPoP access token for the client CSR, given a JWT DPoP proof, specified in the `DPoP` header. The access token will be returned in the JSON response body as a JWT DPoP token.", "parameters": [ { "description": "ClientId", diff --git a/services/brig/docs/swagger-v4.json b/services/brig/docs/swagger-v4.json index 08109f8229a..f1c676b0062 100644 --- a/services/brig/docs/swagger-v4.json +++ b/services/brig/docs/swagger-v4.json @@ -7834,7 +7834,7 @@ }, "/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.", + "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 in the JSON response body as a JWT DPoP token.", "parameters": [ { "description": "ClientId", diff --git a/services/brig/docs/swagger-v5.json b/services/brig/docs/swagger-v5.json index de2f152a787..16ad51530d0 100644 --- a/services/brig/docs/swagger-v5.json +++ b/services/brig/docs/swagger-v5.json @@ -9468,7 +9468,7 @@ }, "/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.", + "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 in the JSON response body as a JWT DPoP token.", "parameters": [ { "description": "ClientId", diff --git a/services/brig/docs/swagger-v6.json b/services/brig/docs/swagger-v6.json index e421a2fbce6..2047bce8d24 100644 --- a/services/brig/docs/swagger-v6.json +++ b/services/brig/docs/swagger-v6.json @@ -9965,7 +9965,7 @@ }, "/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.", + "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 in the JSON response body as a JWT DPoP token.", "parameters": [ { "description": "ClientId", diff --git a/services/brig/docs/swagger-v7.json b/services/brig/docs/swagger-v7.json index 46f5d378379..c7369158fd6 100644 --- a/services/brig/docs/swagger-v7.json +++ b/services/brig/docs/swagger-v7.json @@ -10213,7 +10213,7 @@ }, "/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.", + "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 in the JSON response body as a JWT DPoP token.", "operationId": "create-access-token", "parameters": [ { diff --git a/services/brig/docs/swagger-v8.json b/services/brig/docs/swagger-v8.json index 89dc4fb45b2..50fd587b1dc 100644 --- a/services/brig/docs/swagger-v8.json +++ b/services/brig/docs/swagger-v8.json @@ -10676,7 +10676,7 @@ }, "/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.", + "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 in the JSON response body as a JWT DPoP token.", "operationId": "create-access-token", "parameters": [ { diff --git a/services/brig/docs/swagger-v9.json b/services/brig/docs/swagger-v9.json index 018973ba4e8..32fa90886e9 100644 --- a/services/brig/docs/swagger-v9.json +++ b/services/brig/docs/swagger-v9.json @@ -10727,7 +10727,7 @@ }, "/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.", + "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 in the JSON response body as a JWT DPoP token.", "operationId": "create-access-token", "parameters": [ { diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index 89a0fc641f1..ce43b65844d 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -218,7 +218,8 @@ addClientWithReAuthPolicy policy luid@(tUnqualified -> u) con new = do (Data.addClientWithReAuthPolicy policy luid clientId' new maxPermClients mCaps) !>> ClientDataError let clt = clt0 {clientMLSPublicKeys = newClientMLSPublicKeys new} - when (supportsConsumableNotifications clt) $ lift $ liftSem $ do + consumableNotificationsEnabled <- asks (.settings.consumableNotifications) + when (consumableNotificationsEnabled && supportsConsumableNotifications clt) $ lift $ liftSem $ do setupConsumableNotifications u clt.clientId lift $ do for_ old $ execDelete u con @@ -253,13 +254,14 @@ updateClient :: (Handler r) () updateClient uid cid req = do client <- (lift (liftSem (ClientStore.lookupClient uid cid)) >>= maybe (throwE ClientNotFound) pure) !>> clientError + consumableNotificationsEnabled <- asks (.settings.consumableNotifications) lift . liftSem $ for_ req.updateClientLabel $ ClientStore.updateLabel uid cid . Just for_ req.updateClientCapabilities $ \caps -> do if client.clientCapabilities.fromClientCapabilityList `Set.isSubsetOf` caps.fromClientCapabilityList then do -- first set up the notification queues then save the data is more robust than the other way around let addedCapabilities = caps.fromClientCapabilityList \\ client.clientCapabilities.fromClientCapabilityList - when (ClientSupportsConsumableNotifications `Set.member` addedCapabilities) $ lift $ liftSem $ do + when (consumableNotificationsEnabled && ClientSupportsConsumableNotifications `Set.member` addedCapabilities) $ lift $ liftSem $ do setupConsumableNotifications uid cid lift . liftSem . ClientStore.updateCapabilities uid cid . Just $ caps else throwE $ clientError ClientCapabilitiesCannotBeRemoved diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index adc473693ff..ff8d6d5eb31 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -620,7 +620,8 @@ createUserNoVerifySpar :: Member UserSubsystem r, Member Events r, Member PasswordResetCodeStore r, - Member UserStore r + Member UserStore r, + Member UserKeyStore r ) => NewUserSpar -> (Handler r) (Either CreateUserSparError SelfProfile) diff --git a/services/brig/src/Brig/API/MLS/KeyPackages.hs b/services/brig/src/Brig/API/MLS/KeyPackages.hs index 5a0e11faa3d..03e933cb38c 100644 --- a/services/brig/src/Brig/API/MLS/KeyPackages.hs +++ b/services/brig/src/Brig/API/MLS/KeyPackages.hs @@ -55,6 +55,7 @@ import Wire.API.MLS.Credential import Wire.API.MLS.KeyPackage import Wire.API.MLS.Serialisation import Wire.API.Team.LegalHold +import Wire.API.User (AccountStatus (..)) import Wire.API.User.Client import Wire.ClientStore (ClientStore) import Wire.ClientStore qualified as ClientStore @@ -112,24 +113,27 @@ claimLocalKeyPackages :: CipherSuiteTag -> Local UserId -> ExceptT ClientError (AppT r) KeyPackageBundle -claimLocalKeyPackages qusr skipOwn suite target = do +claimLocalKeyPackages qusr skipOwn suite qTarget = do + let target = tUnqualified qTarget + su <- lift (liftSem $ getUser target) >>= maybe (throwE (ClientUserNotFound target)) pure + when (not su.activated || maybe True ((/=) Active) su.status) $ throwE (ClientUserNotFound target) -- while we do not support federation + MLS together with legalhold, to make sure that -- the remote backend is complicit with our legalhold policies, we disallow anyone -- fetching key packages for users under legalhold -- -- This way we prevent both locally and on the remote to add a user under legalhold to an MLS -- conversation - assertUserNotUnderLegalHold + assertUserNotUnderLegalHold su -- skip own client when the target is the requesting user itself - let own = guard (qusr == tUntagged target) *> skipOwn - clients <- map (.clientId) <$> lift (liftSem (ClientStore.lookupClients (tUnqualified target))) + let own = guard (qusr == tUntagged qTarget) *> skipOwn + clients <- map (.clientId) <$> lift (liftSem (ClientStore.lookupClients target)) foldQualified - target + qTarget ( \lusr -> guardLegalhold (ProtectedUser (tUnqualified lusr)) - (mkUserClients [(tUnqualified target, clients)]) + (mkUserClients [(target, clients)]) ) (\_ -> pure ()) qusr @@ -140,27 +144,23 @@ claimLocalKeyPackages qusr skipOwn suite target = do mkEntry own c = runMaybeT $ do guard $ Just c /= own - uncurry (KeyPackageBundleEntry (tUntagged target) c) - <$> wrapClientM (Data.claimKeyPackage target c suite) + uncurry (KeyPackageBundleEntry (tUntagged qTarget) c) + <$> wrapClientM (Data.claimKeyPackage qTarget c suite) -- FUTUREWORK: shouldn't this be defined elsewhere for general use? - assertUserNotUnderLegalHold :: ExceptT ClientError (AppT r) () - assertUserNotUnderLegalHold = do - -- this is okay because there can only be one StoredUser per UserId - mSu <- lift $ liftSem $ getUser (tUnqualified target) - case mSu of - Nothing -> pure () -- Legalhold is a team feature. - Just su -> - for_ su.teamId $ \tid -> do - resp <- lift $ liftSem $ getUserLegalholdStatus target tid - -- if an admin tries to put a user under legalhold - -- the user has to first reject to be put under legalhold - -- before they can join conversations again - case resp.ulhsrStatus of - UserLegalHoldPending -> throwE ClientLegalHoldIncompatible - UserLegalHoldEnabled -> throwE ClientLegalHoldIncompatible - UserLegalHoldDisabled -> pure () - UserLegalHoldNoConsent -> pure () + assertUserNotUnderLegalHold :: StoredUser -> ExceptT ClientError (AppT r) () + assertUserNotUnderLegalHold su = + for_ su.teamId $ \tid -> do + mLhStatus <- lift $ liftSem $ getUserLegalholdStatus qTarget tid + -- if an admin tries to put a user under legalhold + -- the user has to first reject to be put under legalhold + -- before they can join conversations again + case ulhsrStatus <$> mLhStatus of + Just UserLegalHoldPending -> throwE ClientLegalHoldIncompatible + Just UserLegalHoldEnabled -> throwE ClientLegalHoldIncompatible + Just UserLegalHoldDisabled -> pure () + Just UserLegalHoldNoConsent -> pure () + Nothing -> pure () claimRemoteKeyPackages :: (Member ClientStore r) => diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 09c7dece332..5834e64788a 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -102,7 +102,6 @@ import Servant.OpenApi.Internal.Orphans () import Servant.Swagger.UI import System.Logger.Class qualified as Log import Util.Logging (logFunction, logHandle, logTeam, logUser) -import Wire.API.App import Wire.API.Connection qualified as Public import Wire.API.EnterpriseLogin import Wire.API.Error @@ -140,7 +139,7 @@ import Wire.API.SystemSettings import Wire.API.Team qualified as Public import Wire.API.Team.LegalHold (LegalholdProtectee (..)) import Wire.API.Team.Member (HiddenPerm (..), IsPerm (..), hasPermission) -import Wire.API.User (RegisterError (RegisterErrorAllowlistError)) +import Wire.API.User (RegisterError (RegisterErrorAllowlistError), UserProfile) import Wire.API.User qualified as Public import Wire.API.User.Activation qualified as Public import Wire.API.User.Auth qualified as Public @@ -180,7 +179,6 @@ import Wire.IndexedUserStore (IndexedUserStore) import Wire.InvitationStore import Wire.NotificationSubsystem import Wire.PasswordResetCodeStore (PasswordResetCodeStore) -import Wire.PasswordStore (PasswordStore, lookupHashedPassword) import Wire.PropertySubsystem import Wire.RateLimit import Wire.Sem.Concurrency @@ -237,22 +235,23 @@ internalEndpointsSwaggerDocsAPIs = -- -- Dual to `internalEndpointsSwaggerDocsAPI`. versionedSwaggerDocsAPI :: Servant.Server VersionedSwaggerDocsAPI -versionedSwaggerDocsAPI (Just (VersionNumber V15)) = +versionedSwaggerDocsAPI (Just (VersionNumber V16)) = swaggerSchemaUIServer $ - ( 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 + ( serviceSwagger @VersionAPITag @'V16 + <> serviceSwagger @BrigAPITag @'V16 + <> serviceSwagger @GalleyAPITag @'V16 + <> serviceSwagger @SparAPITag @'V16 + <> serviceSwagger @CargoholdAPITag @'V16 + <> serviceSwagger @CannonAPITag @'V16 + <> serviceSwagger @GundeckAPITag @'V16 + <> serviceSwagger @ProxyAPITag @'V16 + <> serviceSwagger @OAuthAPITag @'V16 ) & S.info . S.title .~ "Wire-Server API" & S.info . S.description ?~ $((unTypeCode . embedText) =<< makeRelativeToProject "docs/swagger.md") - & S.servers .~ [S.Server ("/" <> toUrlPiece V15) Nothing mempty] + & S.servers .~ [S.Server ("/" <> toUrlPiece V16) Nothing mempty] & cleanupSwagger +versionedSwaggerDocsAPI (Just (VersionNumber V15)) = swaggerPregenUIServer $(pregenSwagger V15) versionedSwaggerDocsAPI (Just (VersionNumber V14)) = swaggerPregenUIServer $(pregenSwagger V14) versionedSwaggerDocsAPI (Just (VersionNumber V13)) = swaggerPregenUIServer $(pregenSwagger V13) versionedSwaggerDocsAPI (Just (VersionNumber V12)) = swaggerPregenUIServer $(pregenSwagger V12) @@ -386,7 +385,6 @@ servantSitemap :: Member NotificationSubsystem r, Member Now r, Member PasswordResetCodeStore r, - Member PasswordStore r, Member PropertySubsystem r, Member PublicKeyBundle r, Member SFT r, @@ -1171,12 +1169,11 @@ removeEmail = lift . liftSem . User.removeEmailEither >=> reint Left e -> lift . liftSem . throw $ e Right () -> pure Nothing -checkPasswordExists :: (Member PasswordStore r) => UserId -> (Handler r) Bool -checkPasswordExists = fmap isJust . lift . liftSem . lookupHashedPassword +checkPasswordExists :: (Member UserStore r) => UserId -> (Handler r) Bool +checkPasswordExists = fmap isJust . lift . liftSem . UserStore.lookupHashedPassword changePassword :: - ( Member PasswordStore r, - Member UserStore r, + ( Member UserStore r, Member HashPassword r, Member RateLimit r, Member AuthenticationSubsystem r @@ -1438,7 +1435,6 @@ deleteSelfUser :: Member UserKeyStore r, Member NotificationSubsystem r, Member UserStore r, - Member PasswordStore r, Member EmailSubsystem r, Member UserSubsystem r, Member VerificationCodeSubsystem r, @@ -1523,7 +1519,8 @@ activate :: Member UserSubsystem r, Member Events r, Member PasswordResetCodeStore r, - Member UserStore r + Member UserStore r, + Member UserKeyStore r ) => Public.ActivationKey -> Public.ActivationCode -> @@ -1539,7 +1536,8 @@ activateKey :: Member Events r, Member UserSubsystem r, Member PasswordResetCodeStore r, - Member UserStore r + Member UserStore r, + Member UserKeyStore r ) => Public.Activate -> (Handler r) ActivationRespWithStatus @@ -1769,24 +1767,31 @@ updateUserGroupChannels lusr gid appendOnly upd = checkUserGroupNameAvailable :: Local UserId -> CheckUserGroupName -> Handler r UserGroupNameAvailability checkUserGroupNameAvailable _ _ = pure $ UserGroupNameAvailability True -createApp :: (_) => Local UserId -> TeamId -> NewApp -> Handler r CreatedApp +createApp :: (_) => Local UserId -> TeamId -> Public.NewApp -> Handler r Public.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 +getApp :: (_) => Local UserId -> TeamId -> UserId -> Handler r UserProfile +getApp lusr _tid uid = lift . liftSem $ do + prof <- getLocalUserProfileFiltered404 AppsOnly (qualifyAs lusr uid) + if prof.profileDeleted + then throw UserSubsystemProfileNotFound + else pure prof -getApps :: (_) => Local UserId -> TeamId -> Handler r GetAppList -getApps lusr tid = lift . liftSem $ AppSubsystem.getApps lusr tid +getApps :: (_) => Local UserId -> TeamId -> Handler r [UserProfile] +getApps lusr tid = + lift . liftSem $ do + appIds <- AppSubsystem.getAppIds lusr tid + getLocalUserProfilesFiltered AppsOnly (qualifyAs lusr appIds) -putApp :: (_) => Local UserId -> TeamId -> UserId -> PutApp -> Handler r () +putApp :: (_) => Local UserId -> TeamId -> UserId -> Public.PutApp -> Handler r () putApp lusr tid uid put = lift . liftSem $ AppSubsystem.updateApp lusr tid uid put -refreshAppCookie :: (_) => Local UserId -> TeamId -> UserId -> Handler r RefreshAppCookieResponse -refreshAppCookie lusr tid appId = do - mc <- lift . liftSem $ AppSubsystem.refreshAppCookie lusr tid appId +refreshAppCookie :: (_) => Local UserId -> TeamId -> UserId -> Public.RefreshAppCookieRequest -> Handler r Public.RefreshAppCookieResponse +refreshAppCookie lusr tid appId req = do + mc <- lift . liftSem $ AppSubsystem.refreshAppCookie lusr tid appId req.password case mc of Left delay -> throwE $ loginError (LoginThrottled delay) - Right c -> pure $ RefreshAppCookieResponse c + Right c -> pure $ Public.RefreshAppCookieResponse c -- Deprecated diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index bc0c14f6200..cccbf9f3880 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -137,7 +137,6 @@ import Wire.InvitationStore (InvitationStore, StoredInvitation) import Wire.InvitationStore qualified as InvitationStore import Wire.NotificationSubsystem import Wire.PasswordResetCodeStore (PasswordResetCodeStore) -import Wire.PasswordStore (PasswordStore, lookupHashedPassword, upsertHashedPassword) import Wire.PropertySubsystem as PropertySubsystem import Wire.RateLimit import Wire.Sem.Concurrency @@ -688,7 +687,8 @@ activate :: Member Events r, Member PasswordResetCodeStore r, Member UserSubsystem r, - Member UserStore r + Member UserStore r, + Member UserKeyStore r ) => ActivationTarget -> ActivationCode -> @@ -703,7 +703,8 @@ activateNoVerifyEmailDomain :: Member Events r, Member PasswordResetCodeStore r, Member UserSubsystem r, - Member UserStore r + Member UserStore r, + Member UserKeyStore r ) => ActivationTarget -> ActivationCode -> @@ -718,7 +719,8 @@ activateWithCurrency :: Member Events r, Member PasswordResetCodeStore r, Member UserSubsystem r, - Member UserStore r + Member UserStore r, + Member UserKeyStore r ) => Bool -> ActivationTarget -> @@ -884,8 +886,7 @@ mkActivationKey (ActivateEmail e) = -- Password Management changePassword :: - ( Member PasswordStore r, - Member UserStore r, + ( Member UserStore r, Member HashPassword r, Member RateLimit r, Member AuthenticationSubsystem r @@ -897,12 +898,12 @@ changePassword uid cp = do activated <- lift $ liftSem $ UserStore.isActivated uid unless activated $ throwE ChangePasswordNoIdentity - currpw <- lift $ liftSem $ lookupHashedPassword uid + currpw <- lift $ liftSem $ UserStore.lookupHashedPassword uid let newpw = cp.newPassword rateLimitKey = RateLimitUser uid hashedNewPw <- lift . liftSem $ HashPassword.hashPassword8 rateLimitKey newpw case (currpw, cp.oldPassword) of - (Nothing, _) -> lift . liftSem $ upsertHashedPassword uid hashedNewPw + (Nothing, _) -> lift . liftSem $ UserStore.upsertHashedPassword uid hashedNewPw (Just _, Nothing) -> throwE InvalidCurrentPassword (Just pw, Just pw') -> do -- We are updating the pwd here anyway, so we don't care about the pwd status @@ -910,7 +911,7 @@ changePassword uid cp = do throwE InvalidCurrentPassword whenM (lift . liftSem $ HashPassword.verifyPassword rateLimitKey newpw pw) $ throwE ChangePasswordMustDiffer - lift $ liftSem (upsertHashedPassword uid hashedNewPw >> Auth.revokeAllCookies uid) + lift $ liftSem (UserStore.upsertHashedPassword uid hashedNewPw >> Auth.revokeAllCookies uid) ------------------------------------------------------------------------------- -- User Deletion @@ -933,7 +934,6 @@ deleteSelfUser :: Member (Embed HttpClientIO) r, Member UserKeyStore r, Member NotificationSubsystem r, - Member PasswordStore r, Member UserStore r, Member EmailSubsystem r, Member VerificationCodeSubsystem r, @@ -977,7 +977,7 @@ deleteSelfUser luid@(tUnqualified -> uid) pwd = do lift . liftSem . Log.info $ field "user" (toByteString uid) . msg (val "Attempting account deletion with a password") - actual <- lift $ liftSem $ lookupHashedPassword uid + actual <- lift $ liftSem $ UserStore.lookupHashedPassword uid case actual of Nothing -> throwE DeleteUserInvalidPassword Just p -> do diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index 20cb7da4257..d110b47f644 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -96,7 +96,6 @@ module Brig.App liftSem, lowerAppT, initHttpManagerWithTLSConfig, - adhocUserKeyStoreInterpreter, adhocSessionStoreInterpreter, ) where @@ -171,10 +170,6 @@ import Wire.ExternalAccess.External import Wire.RateLimit.Interpreter import Wire.SessionStore import Wire.SessionStore.Cassandra -import Wire.UserKeyStore -import Wire.UserKeyStore.Cassandra -import Wire.UserStore -import Wire.UserStore.Cassandra schemaVersion :: Int32 schemaVersion = Migrations.lastSchemaVersion @@ -632,12 +627,6 @@ instance HasRequestId (AppT r) where ------------------------------------------------------------------------------- -- Ad hoc interpreters --- | similarly to `wrapClient`, this function serves as a crutch while Brig is being polysemised. -adhocUserKeyStoreInterpreter :: (MonadIO m, MonadReader Env m) => Sem '[UserKeyStore, UserStore, Embed IO] a -> m a -adhocUserKeyStoreInterpreter action = do - clientState <- asks (.casClient) - liftIO $ runM . interpretUserStoreCassandra clientState . interpretUserKeyStoreCassandra clientState $ action - -- | similarly to `wrapClient`, this function serves as a crutch while Brig is being polysemised. adhocSessionStoreInterpreter :: (MonadIO m, MonadReader Env m) => Sem '[SessionStore, Embed IO] a -> m a adhocSessionStoreInterpreter action = do diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index 5b8bcb912fe..c97db46cc34 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -157,14 +157,22 @@ import Wire.VerificationCodeStore.Cassandra import Wire.VerificationCodeSubsystem import Wire.VerificationCodeSubsystem.Interpreter -type BrigCanonicalEffects = - '[ AppSubsystem, - AuthenticationSubsystem, - TeamInvitationSubsystem, +type BrigCanonicalEffects = NonRecursiveEffects1 `Append` RecursiveEffects `Append` NonRecursiveEffects2 + +type NonRecursiveEffects1 = + '[ TeamInvitationSubsystem, EnterpriseLoginSubsystem, - UserGroupSubsystem, + UserGroupSubsystem + ] + +type RecursiveEffects = + '[ AuthenticationSubsystem, UserSubsystem, - TeamCollaboratorsSubsystem + AppSubsystem + ] + +type NonRecursiveEffects2 = + '[ TeamCollaboratorsSubsystem ] `Append` BrigLowerLevelEffects @@ -250,6 +258,27 @@ type BrigLowerLevelEffects = Final IO ] +-- These interpreters depend on each other, we use let recursion to solve that. +-- +-- This terminates if and only if we do not create an action sequence at +-- runtime such that interpretation of actions results in a call cycle. +-- +-- Cloned from "Wire.MiniBackend". +runRecursiveEffects :: + (Members NonRecursiveEffects2 r) => + Sem (RecursiveEffects `Append` r) a -> + Sem r a +runRecursiveEffects = runApp . runUser . runAuth + where + runAuth :: forall r. (Members NonRecursiveEffects2 r) => InterpreterFor AuthenticationSubsystem r + runAuth = interpretAuthenticationSubsystem runUser + + runUser :: forall r. (Members NonRecursiveEffects2 r) => InterpreterFor UserSubsystem r + runUser = runUserSubsystem runAuth runApp + + runApp :: forall r. (Members NonRecursiveEffects2 r) => InterpreterFor AppSubsystem r + runApp = runAppSubsystem runUser runAuth + runBrigToIO :: App.Env -> AppT BrigCanonicalEffects a -> IO a runBrigToIO e (AppT ma) = do let blockedDomains = @@ -331,16 +360,6 @@ runBrigToIO e (AppT ma) = do casClient = e.casClient } - -- These interpreters depend on each other, we use let recursion to solve that. - -- - -- This terminates if and only if we do not create an action sequence at - -- runtime such that interpretation of actions results in a call cycle. - userSubsystemInterpreter :: (Members BrigLowerLevelEffects r) => InterpreterFor UserSubsystem r - userSubsystemInterpreter = runUserSubsystem authSubsystemInterpreter - - authSubsystemInterpreter :: (Members BrigLowerLevelEffects r) => InterpreterFor AuthenticationSubsystem r - authSubsystemInterpreter = interpretAuthenticationSubsystem userSubsystemInterpreter - ( either throwM pure <=< ( runFinal . unsafelyPerformConcurrency @@ -421,15 +440,13 @@ runBrigToIO e (AppT ma) = do . samlEmailSubsystemInterpreter . runClientSubsystem . interpretTeamCollaboratorsSubsystem - . userSubsystemInterpreter + . runRecursiveEffects . interpretUserGroupSubsystem . maybe runEnterpriseLoginSubsystemNoConfig runEnterpriseLoginSubsystemWithConfig (mkEnterpriseLoginSubsystemConfig e) . runTeamInvitationSubsystem teamInvitationSubsystemConfig - . authSubsystemInterpreter - . runAppSubsystem ) ) $ runReaderT ma e diff --git a/services/brig/src/Brig/Data/Activation.hs b/services/brig/src/Brig/Data/Activation.hs index 3fcfc77d8d9..e500f37922b 100644 --- a/services/brig/src/Brig/Data/Activation.hs +++ b/services/brig/src/Brig/Data/Activation.hs @@ -26,7 +26,7 @@ module Brig.Data.Activation ) where -import Brig.App (AppT, adhocUserKeyStoreInterpreter, liftSem, qualifyLocal, wrapClientE) +import Brig.App (AppT, liftSem, qualifyLocal, wrapClientE) import Brig.Types.Intra import Cassandra import Control.Error @@ -73,7 +73,8 @@ activateKey :: forall r. ( Member UserSubsystem r, Member PasswordResetCodeStore r, - Member UserStore r + Member UserStore r, + Member UserKeyStore r ) => ActivationKey -> ActivationCode -> @@ -122,7 +123,7 @@ activateKey k c u = do lift . liftSem $ Password.codeDelete (mkPasswordResetKey uid) claim key uid lift $ updateEmailAndDeleteEmailUnvalidated uid (emailKeyOrig key) - for_ oldKey $ lift . adhocUserKeyStoreInterpreter . deleteKey + for_ oldKey $ lift . liftSem . deleteKey pure . Just $ EmailActivated uid (emailKeyOrig key) where updateEmailAndDeleteEmailUnvalidated :: UserId -> EmailAddress -> AppT r () @@ -131,7 +132,7 @@ activateKey k c u = do claim :: EmailKey -> UserId -> ExceptT ActivationError (AppT r) () claim key uid = do - ok <- lift $ adhocUserKeyStoreInterpreter (claimKey key uid) + ok <- lift $ liftSem (claimKey key uid) unless ok $ throwE . UserKeyExists . LT.fromStrict $ fromEmail (emailKeyOrig key) diff --git a/services/brig/src/Brig/Options.hs b/services/brig/src/Brig/Options.hs index 47122739bd2..91cf542e487 100644 --- a/services/brig/src/Brig/Options.hs +++ b/services/brig/src/Brig/Options.hs @@ -591,7 +591,9 @@ data Settings = Settings -- | Whether to allow ephemeral user creation ephemeralUserCreationEnabled :: !Bool, -- | Determines if this backend supports nomad profiles. - nomadProfiles :: !(Maybe Bool) + nomadProfiles :: !(Maybe Bool), + -- | Determines if consumable notifications are enabled + consumableNotifications :: !Bool } deriving (Show, Generic) diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index 0da0159674b..286393c5e4a 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -924,7 +924,7 @@ botGetSelf :: (Member UserSubsystem r, Member (Input (Local ())) r) => BotId -> botGetSelf bot = do getBy <- lift . liftSem . qualifyLocal' $ getByNoFilters {getByUserId = [botUserId bot], includePendingInvitations = NoPendingInvitations} p <- fmap listToMaybe . lift . liftSem $ User.getAccountsBy getBy - maybe (throwStd (errorToWai @'E.UserNotFound)) (\u -> pure $ Public.mkUserProfile EmailVisibleToSelf UserTypeBot u UserLegalHoldNoConsent) p + maybe (throwStd (errorToWai @'E.UserNotFound)) (\u -> pure $ Public.mkUserProfile EmailVisibleToSelf u Nothing UserLegalHoldNoConsent) p botGetClient :: (Member GalleyAPIAccess r, Member ClientStore r) => BotId -> (Handler r) (Maybe Public.Client) botGetClient bot = do diff --git a/services/brig/src/Brig/User/Auth/Cookie.hs b/services/brig/src/Brig/User/Auth/Cookie.hs index 1983496bcef..6288cf652e0 100644 --- a/services/brig/src/Brig/User/Auth/Cookie.hs +++ b/services/brig/src/Brig/User/Auth/Cookie.hs @@ -62,6 +62,7 @@ import Web.Cookie qualified as WebCookie import Wire.API.User.Auth import Wire.AuthenticationSubsystem import Wire.AuthenticationSubsystem.Config +import Wire.AuthenticationSubsystem.Cookie (revokeAllCookies) import Wire.AuthenticationSubsystem.ZAuth qualified as ZAuth import Wire.Sem.Metrics (Metrics) import Wire.Sem.Metrics qualified as Metrics @@ -198,9 +199,6 @@ listCookies u ll = filter byLabel <$> adhocSessionStoreInterpreter (Store.listCo where byLabel c = maybe False (`elem` ll) (cookieLabel c) -revokeAllCookies :: (Member AuthenticationSubsystem r) => UserId -> Sem r () -revokeAllCookies u = revokeCookies u [] [] - -------------------------------------------------------------------------------- -- HTTP diff --git a/services/brig/test/integration/API/Internal.hs b/services/brig/test/integration/API/Internal.hs deleted file mode 100644 index 555a136b483..00000000000 --- a/services/brig/test/integration/API/Internal.hs +++ /dev/null @@ -1,107 +0,0 @@ --- Disabling to stop warnings on HasCallStack -{-# OPTIONS_GHC -Wno-redundant-constraints #-} - --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module API.Internal - ( tests, - ) -where - -import API.UserPendingActivation (userExists) -import Bilge -import Bilge.Assert -import Brig.Options qualified as Opt -import Cassandra qualified as C -import Cassandra qualified as Cass -import Cassandra.Util -import Control.Monad.Catch -import Data.ByteString.Conversion (toByteString') -import Data.Id -import Data.Qualified -import Imports -import Test.Tasty -import Test.Tasty.HUnit -import Util -import Util.Options (Endpoint) -import Wire.API.User - -type TestConstraints m = (MonadFail m, MonadCatch m, MonadIO m, MonadHttp m) - -tests :: Opt.Opts -> Manager -> Cass.ClientState -> Brig -> Endpoint -> Gundeck -> Galley -> IO TestTree -tests opts mgr db brig brigep _gundeck galley = do - pure $ - testGroup "api/internal" $ - [ test mgr "suspend and unsuspend user" $ testSuspendUser db brig, - test mgr "suspend non existing user and verify no db entry" $ - testSuspendNonExistingUser db brig, - test mgr "writetimeToInt64" $ testWritetimeRepresentation opts mgr db brig brigep galley - ] - -testSuspendUser :: forall m. (TestConstraints m) => Cass.ClientState -> Brig -> m () -testSuspendUser db brig = do - user <- randomUser brig - let checkAccountStatus s = do - mbStatus <- Cass.runClient db (lookupStatus (userId user)) - liftIO $ mbStatus @?= Just s - - setAccountStatus brig (userId user) Suspended !!! const 200 === statusCode - checkAccountStatus Suspended - setAccountStatus brig (userId user) Active !!! const 200 === statusCode - checkAccountStatus Active - -testSuspendNonExistingUser :: forall m. (TestConstraints m) => Cass.ClientState -> Brig -> m () -testSuspendNonExistingUser db brig = do - nonExistingUserId <- randomId - setAccountStatus brig nonExistingUserId Suspended !!! const 404 === statusCode - isUserCreated <- Cass.runClient db (userExists nonExistingUserId) - liftIO $ isUserCreated @?= False - -setAccountStatus :: (MonadHttp m, HasCallStack) => Brig -> UserId -> AccountStatus -> m ResponseLBS -setAccountStatus brig u s = - put - ( brig - . paths ["i", "users", toByteString' u, "status"] - . contentJson - . json (AccountStatusUpdate s) - ) - -testWritetimeRepresentation :: forall m. (TestConstraints m) => Opt.Opts -> Manager -> Cass.ClientState -> Brig -> Endpoint -> Galley -> m () -testWritetimeRepresentation _ _mgr db brig _brigep _galley = do - quid <- userQualifiedId <$> randomUser brig - let uid = qUnqualified quid - - ref <- fromJust <$> (runIdentity <$$> Cass.runClient db (C.query1 q1 (C.params C.LocalQuorum (Identity uid)))) - - wt <- fromJust <$> (runIdentity <$$> Cass.runClient db (C.query1 q2 (C.params C.LocalQuorum (Identity uid)))) - - liftIO $ assertEqual "writetimeToInt64() does not match WRITETIME(status)" ref (writetimeToInt64 wt) - where - q1 :: C.PrepQuery C.R (Identity UserId) (Identity Int64) - q1 = "SELECT WRITETIME(status) from user where id = ?" - - q2 :: C.PrepQuery C.R (Identity UserId) (Identity (Writetime ())) - q2 = "SELECT WRITETIME(status) from user where id = ?" - -lookupStatus :: UserId -> C.Client (Maybe AccountStatus) -lookupStatus u = - (runIdentity =<<) - <$> C.retry C.x1 (C.query1 statusSelect (C.params C.LocalQuorum (Identity u))) - where - statusSelect :: C.PrepQuery C.R (Identity UserId) (Identity (Maybe AccountStatus)) - statusSelect = "SELECT status FROM user WHERE id = ?" diff --git a/services/brig/test/integration/API/User/Account.hs b/services/brig/test/integration/API/User/Account.hs index bf4e98785eb..18ac0d76357 100644 --- a/services/brig/test/integration/API/User/Account.hs +++ b/services/brig/test/integration/API/User/Account.hs @@ -808,6 +808,7 @@ testMultipleUsers opts brig = do profileLegalholdStatus = UserLegalHoldDisabled, profileSupportedProtocols = defSupportedProtocols, profileType = UserTypeRegular, + profileApp = Nothing, profileSearchable = True } users = [u1, u2, u3] diff --git a/services/brig/test/integration/API/User/Auth.hs b/services/brig/test/integration/API/User/Auth.hs index 77c55f59685..9cd8047a67e 100644 --- a/services/brig/test/integration/API/User/Auth.hs +++ b/services/brig/test/integration/API/User/Auth.hs @@ -242,7 +242,7 @@ testNginz b n = do liftIO $ assertEqual "Ensure nginz is started. Ensure nginz and brig share the same private/public zauth keys. Ensure ACL file is correct." 200 (statusCode _rs) -- ensure nginz allows refresh at /access _rs <- - post (unversioned . n . path "/access" . cookie c . header "Authorization" ("Bearer " <> toByteString' t)) toByteString' t)) toByteString' t)) !!! const 200 === statusCode @@ -276,7 +276,7 @@ testNginzLegalHold b g n = do =<< createConversation g (userId alice) [] toByteString' t)) !!! do + post (unversioned . n . path "/access" . Bilge.cookie c . header "Authorization" ("Bearer " <> toByteString' t)) !!! do const 200 === statusCode -- ensure legalhold tokens CANNOT fetch /clients get (n . path "/clients" . header "Authorization" ("Bearer " <> toByteString' t)) !!! const 403 === statusCode @@ -317,16 +317,16 @@ testNginzMultipleCookies o b n = do badCookie2 <- (\c -> c {cookie_value = "SKsjKQbiqxuEugGMWVbq02fNEA7QFdNmTiSa1Y0YMgaEP5tWl3nYHWlIrM5F8Tt7Cfn2Of738C7oeiY8xzPHAC==.v=1.k=1.d=1.t=u.l=.u=13da31b4-c6bb-4561-8fed-07e728fa6cc5.r=f844b420"}) . decodeCookie <$> dologin -- Basic sanity checks - post (unversioned . n . path "/access" . cookie goodCookie) !!! const 200 === statusCode - post (unversioned . n . path "/access" . cookie badCookie1) !!! const 403 === statusCode - post (unversioned . n . path "/access" . cookie badCookie2) !!! const 403 === statusCode + post (unversioned . n . path "/access" . Bilge.cookie goodCookie) !!! const 200 === statusCode + post (unversioned . n . path "/access" . Bilge.cookie badCookie1) !!! const 403 === statusCode + post (unversioned . n . path "/access" . Bilge.cookie badCookie2) !!! const 403 === statusCode -- Sending both cookies should always work, regardless of the order (they are ordered by time) - post (unversioned . n . path "/access" . cookie badCookie1 . cookie goodCookie . cookie badCookie2) !!! const 200 === statusCode - post (unversioned . n . path "/access" . cookie goodCookie . cookie badCookie1 . cookie badCookie2) !!! const 200 === statusCode - post (unversioned . n . path "/access" . cookie badCookie1 . cookie badCookie2 . cookie goodCookie) !!! const 200 === statusCode -- -- Sending a bad cookie and an unparseble one should work too - post (unversioned . n . path "/access" . cookie unparseableCookie . cookie goodCookie) !!! const 200 === statusCode - post (unversioned . n . path "/access" . cookie goodCookie . cookie unparseableCookie) !!! const 200 === statusCode + post (unversioned . n . path "/access" . Bilge.cookie badCookie1 . Bilge.cookie goodCookie . Bilge.cookie badCookie2) !!! const 200 === statusCode + post (unversioned . n . path "/access" . Bilge.cookie goodCookie . Bilge.cookie badCookie1 . Bilge.cookie badCookie2) !!! const 200 === statusCode + post (unversioned . n . path "/access" . Bilge.cookie badCookie1 . Bilge.cookie badCookie2 . Bilge.cookie goodCookie) !!! const 200 === statusCode -- -- Sending a bad cookie and an unparseble one should work too + post (unversioned . n . path "/access" . Bilge.cookie unparseableCookie . Bilge.cookie goodCookie) !!! const 200 === statusCode + post (unversioned . n . path "/access" . Bilge.cookie goodCookie . Bilge.cookie unparseableCookie) !!! const 200 === statusCode -- We want to make sure we are using a cookie that was deleted from the DB but not expired - this way the client -- will still have it in the cookie jar because it did not get overriden @@ -334,10 +334,10 @@ testNginzMultipleCookies o b n = do now <- liftIO getCurrentTime liftIO $ assertBool "cookie should not be expired" (cookie_expiry_time deleted > now) liftIO $ assertBool "cookie should not be expired" (cookie_expiry_time valid > now) - post (unversioned . n . path "/access" . cookie deleted) !!! const 403 === statusCode - post (unversioned . n . path "/access" . cookie valid) !!! const 200 === statusCode - post (unversioned . n . path "/access" . cookie deleted . cookie valid) !!! const 200 === statusCode - post (unversioned . n . path "/access" . cookie valid . cookie deleted) !!! const 200 === statusCode + post (unversioned . n . path "/access" . Bilge.cookie deleted) !!! const 403 === statusCode + post (unversioned . n . path "/access" . Bilge.cookie valid) !!! const 200 === statusCode + post (unversioned . n . path "/access" . Bilge.cookie deleted . Bilge.cookie valid) !!! const 200 === statusCode + post (unversioned . n . path "/access" . Bilge.cookie valid . Bilge.cookie deleted) !!! const 200 === statusCode ------------------------------------------------------------------------------- -- Login @@ -507,11 +507,11 @@ testLegalHoldLogout brig galley = do uid <- prepareLegalHoldUser brig galley _rs <- legalHoldLogin brig (LegalHoldLogin uid (Just defPassword) Nothing) PersistentCookie runZAuth authCfg (randomAccessToken @ZAuth.LU) - post (unversioned . brig . path "/access" . cookie c . header "Authorization" ("Bearer " <> t)) !!! do + post (unversioned . brig . path "/access" . Bilge.cookie c . header "Authorization" ("Bearer " <> t)) !!! do const 403 === statusCode const (Just "Token mismatch") =~= responseBody -- try refresh with a regular AccessToken but a LegalHoldUserCookie @@ -633,7 +633,7 @@ testTokenMismatchLegalhold authCfg brig galley = do _rs <- legalHoldLogin brig (LegalHoldLogin alice (Just defPassword) Nothing) PersistentCookie let c' = decodeCookie _rs t' <- BS.toStrict . (.access) <$> runZAuth authCfg (randomAccessToken @ZAuth.U) - post (unversioned . brig . path "/access" . cookie c' . header "Authorization" ("Bearer " <> t')) !!! do + post (unversioned . brig . path "/access" . Bilge.cookie c' . header "Authorization" ("Bearer " <> t')) !!! do const 403 === statusCode const (Just "Token mismatch") =~= responseBody @@ -655,7 +655,7 @@ testAccessSelfEmailAllowed nginz brig withCookie = do unversioned . nginz . path "/access/self/email" - . maybe id cookie mbCky + . maybe id Bilge.cookie mbCky . header "Authorization" ("Bearer " <> toByteString' tok) put (req . Bilge.json ()) @@ -685,7 +685,7 @@ testAccessSelfEmailDenied zenv nginz brig withCookie = do . nginz . path "/access/self/email" . Bilge.json (EmailUpdate email) - . maybe id cookie mbCky + . maybe id Bilge.cookie mbCky put req !!! errResponse "invalid-credentials" "Missing access token" @@ -723,7 +723,7 @@ getAndTestDBSupersededCookieAndItsValidSuccessor config b n = do liftIO $ threadDelay minAge -- Refresh tokens _rs <- - post (unversioned . n . path "/access" . cookie c) toByteString' token0) - . cookie c + . Bilge.cookie c ) do - post (unversioned . brig . path "/access" . cookie cky) !!! do + post (unversioned . brig . path "/access" . Bilge.cookie cky) !!! do const 403 === statusCode const Nothing === getHeader "Set-Cookie" "/login" -> do @@ -1091,11 +1091,11 @@ testLogout b = do Just email <- userEmail <$> randomUser b _rs <- login b (defEmailLogin email) SessionCookie let (t, c) = (decodeToken _rs, decodeCookie _rs) - post (unversioned . b . path "/access" . cookie c) + post (unversioned . b . path "/access" . Bilge.cookie c) !!! const 200 === statusCode - post (unversioned . b . path "/access/logout" . cookie c . queryItem "access_token" (toByteString' t)) + post (unversioned . b . path "/access/logout" . Bilge.cookie c . queryItem "access_token" (toByteString' t)) !!! const 200 === statusCode - post (unversioned . b . path "/access" . cookie c) + post (unversioned . b . path "/access" . Bilge.cookie c) !!! const 403 === statusCode testReauthentication :: Brig -> Http () diff --git a/services/brig/test/integration/API/User/Util.hs b/services/brig/test/integration/API/User/Util.hs index 9cf7ede4c2a..97be590cba8 100644 --- a/services/brig/test/integration/API/User/Util.hs +++ b/services/brig/test/integration/API/User/Util.hs @@ -167,7 +167,7 @@ initiateEmailUpdateCreds brig email (cky, tok) uid = do unversioned . brig . path "/access/self/email" - . cookie cky + . Bilge.cookie cky . header "Authorization" ("Bearer " <> toByteString' tok) . zUser uid . Bilge.json (EmailUpdate email) diff --git a/services/brig/test/integration/API/UserPendingActivation.hs b/services/brig/test/integration/API/UserPendingActivation.hs deleted file mode 100644 index 0c4cfabf1fc..00000000000 --- a/services/brig/test/integration/API/UserPendingActivation.hs +++ /dev/null @@ -1,360 +0,0 @@ -{-# LANGUAGE PartialTypeSignatures #-} -{-# LANGUAGE RecordWildCards #-} -{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} - --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module API.UserPendingActivation where - -import API.Team.Util (getTeam) -import Bilge hiding (query) -import Bilge.Assert (( Manager -> ClientState -> Brig -> Galley -> Spar -> IO TestTree -tests opts m db brig galley spar = do - pure $ - testGroup - "cleanExpiredPendingInvitations" - [ test m "expired users get cleaned" (testCleanExpiredPendingInvitations opts db brig galley spar), - test m "users that register dont get cleaned" (testRegisteredUsersNotCleaned opts db brig galley spar) - ] - -testCleanExpiredPendingInvitations :: Opts -> ClientState -> Brig -> Galley -> Spar -> Http () -testCleanExpiredPendingInvitations opts db brig galley spar = do - (owner, tid) <- createUserWithTeamDisableSSO brig galley - tok <- createScimToken spar owner - uid <- do - email <- randomEmail - scimUser <- lift (randomScimUser <&> \u -> u {Scim.User.externalId = Just $ fromEmail email}) - (scimStoredUser, _inv, _inviteeCode) <- createUserStep spar brig tok tid scimUser email - pure $ (Scim.id . Scim.thing) scimStoredUser - assertUserExist "user should exist" db uid True - waitUserExpiration opts - assertUserExist "user should be removed" db uid False - -testRegisteredUsersNotCleaned :: Opts -> ClientState -> Brig -> Galley -> Spar -> Http () -testRegisteredUsersNotCleaned opts db brig galley spar = do - (owner, tid) <- createUserWithTeamDisableSSO brig galley - tok <- createScimToken spar owner - email <- randomEmail - scimUser <- lift (randomScimUser <&> \u -> u {Scim.User.externalId = Just $ fromEmail email}) - (scimStoredUser, _inv, inviteeCode) <- createUserStep spar brig tok tid scimUser email - let uid = (Scim.id . Scim.thing) scimStoredUser - assertUserExist "user should exist" db uid True - registerInvitation brig email (Name "Alice") inviteeCode True - waitUserExpiration opts - assertUserExist "user should still exist" db uid True - -createScimToken :: Spar -> UserId -> HttpT IO ScimToken -createScimToken spar' owner = do - CreateScimTokenResponse tok _ <- - createToken spar' owner $ - CreateScimToken - { description = "testCreateToken", - password = Just defPassword, - verificationCode = Nothing, - name = Nothing, - idp = Nothing - } - pure tok - -createUserStep :: Spar -> Brig -> ScimToken -> TeamId -> Scim.User.User SparTag -> EmailAddress -> HttpT IO (WithMeta (WithId UserId (Scim.User.User SparTag)), Invitation, InvitationCode) -createUserStep spar' brig' tok tid scimUser email = do - scimStoredUser <- createUser spar' tok scimUser - inv <- getInvitationByEmail brig' email - Just inviteeCode <- getInvitationCode brig' tid inv.invitationId - pure (scimStoredUser, inv, inviteeCode) - -assertUserExist :: (HasCallStack) => String -> ClientState -> UserId -> Bool -> HttpT IO () -assertUserExist msg db' uid shouldExist = liftIO $ do - exists <- aFewTimes 12 (runClient db' (userExists uid)) (== shouldExist) - assertEqual msg shouldExist exists - -waitUserExpiration :: (MonadUnliftIO m) => Opts -> m () -waitUserExpiration opts' = do - let timeoutSecs = round @Double . realToFrac $ opts'.settings.teamInvitationTimeout - Control.Exception.assert (timeoutSecs < 30) $ do - threadDelay $ (timeoutSecs + 3) * 1_000_000 - -userExists :: (MonadClient m) => UserId -> m Bool -userExists uid = do - x <- retry x1 (query1 usersSelect (params LocalQuorum (Identity uid))) - pure $ - case x of - Nothing -> False - Just (_, mbStatus) -> - Just Deleted /= mbStatus - where - usersSelect :: PrepQuery R (Identity UserId) (UserId, Maybe AccountStatus) - usersSelect = "SELECT id, status FROM user where id = ?" - -getInvitationByEmail :: Brig -> EmailAddress -> Http Invitation -getInvitationByEmail brig email = - responseJsonUnsafe - <$> ( Bilge.get (brig . path "/i/teams/invitations/by-email" . contentJson . queryItem "email" (toByteString' email)) - Brig -> Galley -> m (UserId, TeamId) -createUserWithTeamDisableSSO brg gly = do - e <- randomEmail - n <- UUID.toString <$> liftIO UUID.nextRandom - let p = - RequestBodyLBS - . Aeson.encode - $ object - [ "name" .= n, - "email" .= fromEmail e, - "password" .= defPassword, - "team" .= newTeam - ] - bdy <- selfUser . responseJsonUnsafe <$> post (brg . path "/i/users" . contentJson . body p) - let (uid, Just tid) = (userId bdy, userTeam bdy) - team <- Team.tdTeam <$> getTeam gly tid - () <- - Control.Exception.assert {- "Team ID in registration and team table do not match" -} (tid == team ^. teamId) $ - pure () - selfTeam <- userTeam . selfUser <$> getSelfProfile brg uid - () <- - Control.Exception.assert {- "Team ID in self profile and team table do not match" -} (selfTeam == Just tid) $ - pure () - pure (uid, tid) - -randomScimUser :: (HasCallStack, MonadRandom m, MonadIO m) => m (Scim.User.User SparTag) -randomScimUser = fst <$> randomScimUserWithSubject - --- | Like 'randomScimUser', but also returns the intended subject ID that the user should --- have. It's already available as 'Scim.User.externalId' but it's not structured. -randomScimUserWithSubject :: - (HasCallStack, MonadRandom m, MonadIO m) => - m (Scim.User.User SparTag, SAML.UnqualifiedNameID) -randomScimUserWithSubject = do - randomScimUserWithSubjectAndRichInfo =<< liftIO (generate arbitrary) - --- | See 'randomScimUser', 'randomScimUserWithSubject'. -randomScimUserWithSubjectAndRichInfo :: - (HasCallStack, MonadRandom m) => - RichInfo -> - m (Scim.User.User SparTag, SAML.UnqualifiedNameID) -randomScimUserWithSubjectAndRichInfo richInfo = do - suffix <- cs <$> replicateM 7 (getRandomR ('0', '9')) - _emails <- getRandomR (0, 3) >>= \n -> replicateM n randomScimEmail - phones <- getRandomR (0, 3) >>= \n -> replicateM n randomScimPhone - -- Related, but non-trivial to re-use here: 'nextSubject' - (externalId, subj) <- - getRandomR (0, 1 :: Int) <&> \case - 0 -> - ( "scimuser_extid_" <> suffix <> "@example.com", - either (error . show) id $ - SAML.mkUNameIDEmail ("scimuser_extid_" <> suffix <> "@example.com") - ) - 1 -> - ( "scimuser_extid_" <> suffix, - SAML.mkUNameIDUnspecified ("scimuser_extid_" <> suffix) - ) - _ -> error "randomScimUserWithSubject: impossible" - pure - ( (Scim.User.empty @SparTag userSchemas ("scimuser_" <> suffix) (ScimUserExtra richInfo)) - { Scim.User.displayName = Just ("ScimUser" <> suffix), - Scim.User.externalId = Just externalId, - Scim.User.phoneNumbers = phones - }, - subj - ) - -randomScimEmail :: (MonadRandom m) => m EmailAddress -randomScimEmail = do - localpart <- cs <$> replicateM 15 (getRandomR ('a', 'z')) - domainpart <- (<> ".com") . cs <$> replicateM 15 (getRandomR ('a', 'z')) - pure $ Email.unsafeEmailAddress localpart domainpart - -randomScimPhone :: (MonadRandom m) => m Phone.Phone -randomScimPhone = do - let typ :: Maybe Text = Nothing - value :: Maybe Text <- do - let mkdigits n = replicateM n (getRandomR ('0', '9')) - mini <- mkdigits 8 - maxi <- mkdigits =<< getRandomR (0, 7) - pure $ Just (cs ('+' : mini <> maxi)) - pure Phone.Phone {..} - --- | Create a user. -createUser :: - (HasCallStack) => - Spar -> - ScimToken -> - Scim.User.User SparTag -> - Http (Scim.StoredUser SparTag) -createUser spar tok user = do - r <- - createUser_ spar (Just tok) user - -- | Authentication - Maybe ScimToken -> - -- | User data - Scim.User.User SparTag -> - -- | Spar endpoint - Http ResponseLBS -createUser_ spar auth user = do - -- NB: we don't use 'mkEmailRandomLocalSuffix' here, because emails - -- shouldn't be submitted via SCIM anyway. - -- TODO: what's the consequence of this? why not update emails via - -- SCIM? how else should they be submitted? i think this there is - -- still some confusion here about the distinction between *validated* - -- emails and *scim-provided* emails, which are two entirely - -- different things. - post $ - ( spar - . paths ["scim", "v2", "Users"] - . scimAuth auth - . contentScim - . json user - . acceptScim - ) - --- | Add SCIM authentication to a request. -scimAuth :: Maybe ScimToken -> Request -> Request -scimAuth Nothing = id -scimAuth (Just auth) = header "Authorization" (toHeader auth) - --- | Signal that the body is an SCIM payload. -contentScim :: Request -> Request -contentScim = content "application/scim+json" - --- | Signal that the response type is expected to be an SCIM payload. -acceptScim :: Request -> Request -acceptScim = accept "application/scim+json" - -getInvitationCode :: - (MonadIO m, MonadHttp m, HasCallStack) => - Brig -> - TeamId -> - InvitationId -> - m (Maybe InvitationCode) -getInvitationCode brig t ref = do - r <- - Bilge.get - ( brig - . path "/i/teams/invitation-code" - . queryItem "team" (toByteString' t) - . queryItem "invitation_id" (toByteString' ref) - ) - let lbs = fromMaybe "" $ responseBody r - pure $ fromByteString (maybe (error "No code?") encodeUtf8 (lbs ^? key "code" . _String)) - --- | Create a SCIM token. -createToken_ :: - Spar -> - -- | User - UserId -> - CreateScimToken -> - -- | Spar endpoint - Http ResponseLBS -createToken_ spar userid payload = do - post $ - ( spar - . paths ["scim", "auth-tokens"] - . zUser userid - . contentJson - . json payload - . acceptJson - ) - --- | Create a SCIM token. -createToken :: - (HasCallStack) => - Spar -> - UserId -> - CreateScimToken -> - Http CreateScimTokenResponse -createToken spar zusr payload = do - r <- - createToken_ - spar - zusr - payload - EmailAddress -> Name -> InvitationCode -> Bool -> Http () -registerInvitation brig email name inviteeCode shouldSucceed = do - void $ - post - ( brig - . path "/register" - . contentJson - . json (acceptWithName name email inviteeCode) - . header "X-Forwarded-For" "127.0.0.42" - ) - EmailAddress -> InvitationCode -> Aeson.Value -acceptWithName name email code = - Aeson.object - [ "name" Aeson..= fromName name, - "email" Aeson..= fromEmail email, - "password" Aeson..= defPassword, - "team_code" Aeson..= code - ] diff --git a/services/brig/test/integration/Run.hs b/services/brig/test/integration/Run.hs index ec143845f3c..2fd4e49c3a8 100644 --- a/services/brig/test/integration/Run.hs +++ b/services/brig/test/integration/Run.hs @@ -22,7 +22,6 @@ where import API.Calling qualified as Calling import API.Federation qualified -import API.Internal qualified import API.Metrics qualified as Metrics import API.OAuth qualified import API.Provider qualified as Provider @@ -32,7 +31,6 @@ import API.Team qualified as Team import API.TeamUserSearch qualified as TeamUserSearch import API.Template qualified import API.User qualified as User -import API.UserPendingActivation qualified as UserPendingActivation import Bilge hiding (header, host, port) import Bilge qualified import Brig.AWS qualified as AWS @@ -115,11 +113,9 @@ runTests :: Config -> Opts.Opts -> [String] -> IO () runTests iConf brigOpts otherArgs = do let b = mkVersionedRequest $ brig iConf c = mkVersionedRequest $ cannon iConf - gd = mkVersionedRequest $ gundeck iConf ch = mkVersionedRequest $ cargohold iConf g = mkVersionedRequest $ galley iConf n = mkVersionedRequest $ nginz iConf - s = mkVersionedRequest $ spar iConf f = federatorInternal iConf brigTwo = mkVersionedRequest $ remoteBrig (backendTwo iConf) cannonTwo = mkVersionedRequest $ remoteCannon (backendTwo iConf) @@ -147,10 +143,8 @@ runTests iConf brigOpts otherArgs = do settingsApi <- Settings.tests brigOpts mg b g createIndex <- Index.Create.spec brigOpts browseTeam <- TeamUserSearch.tests brigOpts mg g b - userPendingActivation <- UserPendingActivation.tests brigOpts mg db b g s federationEnd2End <- Federation.End2end.spec brigOpts mg b g ch c f brigTwo galleyTwo ch2 cannonTwo federationEndpoints <- API.Federation.tests mg brigOpts b fedBrigClient - internalApi <- API.Internal.tests brigOpts mg db b (brig iConf) gd g emailTemplates <- API.Template.tests brigOpts mg let smtp = SMTP.tests mg lg @@ -167,10 +161,8 @@ runTests iConf brigOpts otherArgs = do metricsApi, settingsApi, createIndex, - userPendingActivation, browseTeam, federationEndpoints, - internalApi, smtp, oauthAPI, federationEnd2End, diff --git a/services/federator/test/integration/Test/Federator/IngressSpec.hs b/services/federator/test/integration/Test/Federator/IngressSpec.hs index be2b1c16270..483e9cff179 100644 --- a/services/federator/test/integration/Test/Federator/IngressSpec.hs +++ b/services/federator/test/integration/Test/Federator/IngressSpec.hs @@ -59,7 +59,7 @@ spec env = do brig <- view teBrig <$> ask user <- randomUser brig - let expectedProfile = mkUserProfile EmailVisibleToSelf UserTypeRegular user UserLegalHoldNoConsent + let expectedProfile = mkUserProfile EmailVisibleToSelf user Nothing UserLegalHoldNoConsent runTestSem $ do resp <- liftToCodensity diff --git a/services/federator/test/integration/Test/Federator/InwardSpec.hs b/services/federator/test/integration/Test/Federator/InwardSpec.hs index 180d41b9813..53e01c2bf07 100644 --- a/services/federator/test/integration/Test/Federator/InwardSpec.hs +++ b/services/federator/test/integration/Test/Federator/InwardSpec.hs @@ -70,7 +70,7 @@ spec env = brig <- view teBrig <$> ask user <- randomUser brig - let expectedProfile = mkUserProfile EmailVisibleToSelf UserTypeRegular user UserLegalHoldNoConsent + let expectedProfile = mkUserProfile EmailVisibleToSelf user Nothing UserLegalHoldNoConsent bdy <- responseJsonError =<< inwardCall "/federation/brig/get-users-by-ids" (encode [userId user]) diff --git a/services/galley/default.nix b/services/galley/default.nix index dde0afe0b97..2047f0765cd 100644 --- a/services/galley/default.nix +++ b/services/galley/default.nix @@ -178,8 +178,6 @@ mkDerivation { retry safe-exceptions servant - servant-client - servant-client-core servant-server singletons sop-core diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index f0586d727e9..a05868c9b2f 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -82,6 +82,7 @@ library Galley.API.Create Galley.API.CustomBackend Galley.API.Federation + Galley.API.Federation.Handlers Galley.API.Internal Galley.API.LegalHold Galley.API.LegalHold.Conflicts @@ -92,7 +93,6 @@ library Galley.API.Message Galley.API.MLS Galley.API.MLS.CheckClients - Galley.API.MLS.Commit Galley.API.MLS.Commit.Core Galley.API.MLS.Commit.ExternalCommit Galley.API.MLS.Commit.InternalCommit @@ -126,7 +126,6 @@ library Galley.API.Public.TeamConversation Galley.API.Public.TeamMember Galley.API.Public.TeamNotification - Galley.API.Push Galley.API.Query Galley.API.Teams Galley.API.Teams.Export @@ -136,24 +135,10 @@ library Galley.API.Update Galley.App Galley.Cassandra - Galley.Cassandra.CustomBackend - Galley.Cassandra.Queries - Galley.Cassandra.SearchVisibility - Galley.Cassandra.Store - Galley.Cassandra.Team - Galley.Cassandra.TeamNotifications - Galley.Cassandra.Util - Galley.Data.TeamNotifications - Galley.Effects - Galley.Effects.CustomBackendStore Galley.Effects.Queue - Galley.Effects.SearchVisibilityStore - Galley.Effects.TeamMemberStore - Galley.Effects.TeamNotificationStore Galley.Env Galley.External.LegalHoldService Galley.External.LegalHoldService.Internal - Galley.Intra.Util Galley.Keys Galley.Monad Galley.Options @@ -297,8 +282,6 @@ library , retry >=0.5 , safe-exceptions >=0.1 , servant - , servant-client - , servant-client-core , servant-server , singletons , sop-core diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index f0858b49e9b..9348d5655c9 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -20,12 +20,24 @@ module Galley.API.Action ConversationActionTag (..), ConversationJoin (..), ConversationMemberUpdate (..), - HasConversationActionEffects, HasConversationActionGalleyErrors, -- * Performing actions - updateLocalConversation, - updateLocalConversationUnchecked, + updateLocalConversationUncheckedJoin, + updateLocalConversationUncheckedRemoveMembers, + updateLocalConversationJoin, + updateLocalConversationLeave, + updateLocalConversationMemberUpdate, + updateLocalConversationDelete, + updateLocalConversationRename, + updateLocalConversationMessageTimerUpdate, + updateLocalConversationReceiptModeUpdate, + updateLocalConversationAccessData, + updateLocalConversationRemoveMembers, + updateLocalConversationUpdateProtocol, + updateLocalConversationUpdateAddPermission, + updateLocalConversationReset, + updateLocalConversationHistoryUpdate, NoChanges (..), LocalConversationUpdate (..), notifyTypingIndicator, @@ -69,9 +81,6 @@ import Galley.API.MLS.Conversation import Galley.API.MLS.Migration import Galley.API.MLS.Removal import Galley.API.Teams.Features.Get -import Galley.Effects -import Galley.Env (Env) -import Galley.Options (Opts) import Galley.Types.Error import Imports hiding ((\\)) import Polysemy @@ -98,344 +107,633 @@ 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.History import Wire.API.MLS.Group.Serialisation qualified as Serialisation import Wire.API.MLS.SubConversation import Wire.API.Push.V2 qualified as PushV2 import Wire.API.Routes.Internal.Brig.Connection +import Wire.API.Routes.Public.Galley.MLS import Wire.API.Team.LegalHold import Wire.API.Team.Member import Wire.API.Team.Permission (Perm (AddRemoveConvMember, ModifyConvName)) import Wire.API.User as User +import Wire.BackendNotificationQueueAccess import Wire.BrigAPIAccess qualified as E import Wire.CodeStore import Wire.CodeStore qualified as E import Wire.ConversationStore qualified as E import Wire.ConversationSubsystem import Wire.ConversationSubsystem.Util +import Wire.ExternalAccess import Wire.FeaturesConfigSubsystem import Wire.FederationAPIAccess qualified as E import Wire.FederationSubsystem import Wire.FireAndForget qualified as E +import Wire.LegalHoldStore (LegalHoldStore) import Wire.NotificationSubsystem +import Wire.ProposalStore (ProposalStore) import Wire.ProposalStore qualified as E import Wire.Sem.Now (Now) import Wire.Sem.Now qualified as Now +import Wire.Sem.Random (Random) 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 import Wire.UserList -type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Constraint where - HasConversationActionEffects 'ConversationJoinTag r = - ( -- TODO: Replace with subsystems - Member BrigAPIAccess r, - Member (Error FederationError) r, - Member (Error InternalError) r, - Member (ErrorS 'NotATeamMember) r, - Member (ErrorS 'NotConnected) r, - Member (ErrorS ('ActionDenied 'LeaveConversation)) r, - Member (ErrorS ('ActionDenied 'AddConversationMember)) r, - Member (ErrorS 'InvalidOperation) r, - Member (ErrorS 'ConvAccessDenied) r, - Member (ErrorS 'ConvNotFound) r, - Member (ErrorS 'TooManyMembers) r, - Member (ErrorS 'MissingLegalholdConsent) r, - Member (ErrorS 'GroupIdVersionNotSupported) r, - Member (Error NonFederatingBackends) r, - Member (Error UnreachableBackends) r, - Member ExternalAccess r, - Member (FederationAPIAccess FederatorClient) r, - Member NotificationSubsystem r, - Member (Input ConversationSubsystemConfig) r, - Member Now r, - Member LegalHoldStore r, - Member ConversationStore r, - Member ProposalStore r, - Member Random r, - Member TeamStore r, - Member TinyLog r, - Member ConversationStore r, - Member (Error NoChanges) r - ) - HasConversationActionEffects 'ConversationLeaveTag r = - ( Member (Error InternalError) r, - Member (Error NoChanges) r, - Member ExternalAccess 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, - Member TinyLog r - ) - HasConversationActionEffects 'ConversationRemoveMembersTag r = - ( Member (Error NoChanges) r, - Member ConversationStore r, - Member ProposalStore r, - Member (Input Env) r, - Member (Input ConversationSubsystemConfig) r, - Member Now r, - Member ExternalAccess r, - Member (FederationAPIAccess FederatorClient) r, - Member NotificationSubsystem r, - Member (Error InternalError) r, - Member Random r, - Member TinyLog r, - Member (Error NoChanges) r - ) - HasConversationActionEffects 'ConversationMemberUpdateTag r = - ( Member (ErrorS 'ConvMemberNotFound) r, - Member ConversationStore r - ) - HasConversationActionEffects 'ConversationDeleteTag r = - ( Member BrigAPIAccess r, - Member CodeStore r, - Member ConversationStore r, - Member (Error FederationError) r, - Member (ErrorS 'NotATeamMember) r, - Member (FederationAPIAccess FederatorClient) r, - Member ProposalStore r, - Member TeamStore r - ) - HasConversationActionEffects 'ConversationRenameTag r = - ( Member (Error InvalidInput) r, - Member ConversationStore r, - Member TeamStore r, - Member (ErrorS InvalidOperation) r - ) - HasConversationActionEffects 'ConversationAccessDataTag r = - ( Member BrigAPIAccess r, - Member CodeStore r, - Member (Error InternalError) r, - Member (Error InvalidInput) r, - Member (Error NoChanges) r, - Member (ErrorS 'InvalidTargetAccess) r, - Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, - Member ExternalAccess r, - Member (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, - Member Now r, - Member ConversationStore r, - Member Random r - ) - HasConversationActionEffects 'ConversationHistoryUpdateTag r = - ( Member ConversationStore r, - Member (ErrorS HistoryNotSupported) r - ) - HasConversationActionEffects 'ConversationMessageTimerUpdateTag r = - ( Member ConversationStore r, - Member (Error NoChanges) r - ) - HasConversationActionEffects 'ConversationReceiptModeUpdateTag r = - ( Member ConversationStore r, - Member (Error NoChanges) r, - Member (ErrorS MLSReadReceiptsNotAllowed) r - ) - HasConversationActionEffects 'ConversationUpdateProtocolTag r = - ( Member ConversationStore r, - Member (ErrorS 'ConvInvalidProtocolTransition) r, - Member (ErrorS 'MLSMigrationCriteriaNotSatisfied) r, - Member (Error NoChanges) r, - Member BrigAPIAccess r, - Member ExternalAccess r, - Member (FederationAPIAccess FederatorClient) r, - Member NotificationSubsystem r, - Member (Input Env) r, - Member (Input Opts) r, - Member Now r, - Member ProposalStore r, - Member Random r, - Member TeamFeatureStore r, - Member TinyLog r, - Member FeaturesConfigSubsystem r - ) - HasConversationActionEffects 'ConversationUpdateAddPermissionTag r = - ( Member (Error NoChanges) r, - Member ConversationStore r, - Member (ErrorS 'InvalidTargetAccess) r - ) - HasConversationActionEffects 'ConversationResetTag r = - ( Member (Input Env) r, - Member Now r, +class IsConversationAction (tag :: ConversationActionTag) where + type HasConversationActionEffects tag (r :: EffectRow) :: Constraint + + type HasConversationActionGalleyErrors tag :: EffectRow + + performAction :: + forall r. + ( HasConversationActionEffects tag r, + SingI tag + ) => + Local StoredConversation -> + Qualified UserId -> + Maybe ConnId -> + ConversationAction tag -> + Sem r (PerformActionResult tag) + + ensureAllowed :: + forall mem r x. + ( IsConvMember mem, + HasConversationActionEffects tag r, Member (ErrorS ConvNotFound) r, - Member (ErrorS InvalidOperation) r, - Member ConversationStore r, - Member ExternalAccess r, - Member (FederationAPIAccess FederatorClient) r, - Member NotificationSubsystem r, - Member ProposalStore r, - Member Random r, - Member Resource r, - Member TinyLog r, - Member (ErrorS MLSStaleMessage) r - ) + Member (Error FederationError) r, + Member TeamSubsystem r + ) => + Local x -> + ConversationAction tag -> + StoredConversation -> + ActorContext mem -> + Sem r () + + skipConversationRoleCheck :: StoredConversation -> Maybe TeamMember -> Bool + skipConversationRoleCheck _ _ = False + + -- allowChannelManagePermission is necessary to let team admins act as "channel admins" even if their conversation_role isn't wire_admin, + -- but only for the intended actions. It’s placed here so we bypass only the generic role check and still enforce + -- all channel- and protocol-specific rules afterwards. + allowChannelManagePermission :: Bool + allowChannelManagePermission = False + +instance IsConversationAction 'ConversationJoinTag where + type + HasConversationActionEffects 'ConversationJoinTag r = + ( -- TODO: Replace with subsystems + Member BackendNotificationQueueAccess r, + Member ConversationSubsystem r, + Member TeamCollaboratorsSubsystem r, + Member FederationSubsystem r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r, + Member E.BrigAPIAccess r, + Member (Error FederationError) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS 'NotConnected) r, + Member (ErrorS ('ActionDenied 'AddConversationMember)) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'ConvAccessDenied) r, + Member (ErrorS 'TooManyMembers) r, + Member (ErrorS 'MissingLegalholdConsent) r, + Member (ErrorS 'GroupIdVersionNotSupported) r, + Member (Error UnreachableBackends) r, + Member ExternalAccess r, + Member (E.FederationAPIAccess FederatorClient) r, + Member NotificationSubsystem r, + Member Now r, + Member LegalHoldStore r, + Member ProposalStore r, + Member Random r, + Member TeamStore r, + Member TinyLog r, + Member E.ConversationStore r, + Member (Error NoChanges) r + ) + + type + HasConversationActionGalleyErrors 'ConversationJoinTag = + '[ ErrorS ('ActionDenied 'AddConversationMember), + ErrorS 'GroupIdVersionNotSupported, + ErrorS 'NotATeamMember, + ErrorS 'InvalidOperation, + ErrorS 'ConvNotFound, + ErrorS 'NotConnected, + ErrorS 'ConvAccessDenied, + ErrorS 'TooManyMembers, + ErrorS 'MissingLegalholdConsent + ] + + performAction lconv qusr _conId action = do + (extraTargets, action') <- performConversationJoin qusr lconv action + pure + PerformActionResult + { extraTargets = extraTargets, + action = action', + extraConversationData = def + } -type family HasConversationActionGalleyErrors (tag :: ConversationActionTag) :: EffectRow where - HasConversationActionGalleyErrors 'ConversationJoinTag = - '[ ErrorS ('ActionDenied 'LeaveConversation), - ErrorS ('ActionDenied 'AddConversationMember), - ErrorS 'GroupIdVersionNotSupported, - ErrorS 'NotATeamMember, - ErrorS 'InvalidOperation, - ErrorS 'ConvNotFound, - ErrorS 'NotConnected, - ErrorS 'ConvAccessDenied, - ErrorS 'TooManyMembers, - ErrorS 'MissingLegalholdConsent - ] - HasConversationActionGalleyErrors 'ConversationLeaveTag = - '[ ErrorS ('ActionDenied 'LeaveConversation), - ErrorS 'InvalidOperation, - ErrorS 'ConvNotFound - ] - HasConversationActionGalleyErrors 'ConversationRemoveMembersTag = - '[ ErrorS ('ActionDenied 'RemoveConversationMember), - ErrorS 'InvalidOperation, - ErrorS 'ConvNotFound - ] - HasConversationActionGalleyErrors 'ConversationMemberUpdateTag = - '[ ErrorS ('ActionDenied 'ModifyOtherConversationMember), - ErrorS 'InvalidOperation, - ErrorS 'ConvNotFound, - ErrorS 'ConvMemberNotFound - ] - HasConversationActionGalleyErrors 'ConversationDeleteTag = - '[ ErrorS ('ActionDenied 'DeleteConversation), - ErrorS 'NotATeamMember, - ErrorS 'InvalidOperation, - ErrorS 'ConvNotFound - ] - HasConversationActionGalleyErrors 'ConversationRenameTag = - '[ ErrorS ('ActionDenied 'ModifyConversationName), - ErrorS 'InvalidOperation, - ErrorS 'ConvNotFound - ] - HasConversationActionGalleyErrors 'ConversationMessageTimerUpdateTag = - '[ ErrorS ('ActionDenied 'ModifyConversationMessageTimer), - ErrorS 'InvalidOperation, - ErrorS 'ConvNotFound - ] - HasConversationActionGalleyErrors 'ConversationReceiptModeUpdateTag = - '[ ErrorS ('ActionDenied 'ModifyConversationReceiptMode), - ErrorS 'InvalidOperation, - ErrorS 'MLSReadReceiptsNotAllowed, - ErrorS 'ConvNotFound - ] - HasConversationActionGalleyErrors 'ConversationAccessDataTag = - '[ ErrorS ('ActionDenied 'RemoveConversationMember), - ErrorS ('ActionDenied 'ModifyConversationAccess), - ErrorS 'InvalidOperation, - ErrorS 'InvalidTargetAccess, - ErrorS 'ConvNotFound - ] - HasConversationActionGalleyErrors 'ConversationUpdateProtocolTag = - '[ ErrorS ('ActionDenied 'LeaveConversation), - ErrorS 'InvalidOperation, - ErrorS 'ConvNotFound, - ErrorS 'ConvInvalidProtocolTransition, - ErrorS 'MLSMigrationCriteriaNotSatisfied, - ErrorS 'NotATeamMember, - ErrorS OperationDenied, - ErrorS 'TeamNotFound - ] - HasConversationActionGalleyErrors 'ConversationUpdateAddPermissionTag = - '[ ErrorS ('ActionDenied 'ModifyAddPermission), - ErrorS 'InvalidOperation, - ErrorS 'ConvNotFound, - ErrorS 'NotATeamMember, - ErrorS OperationDenied, - ErrorS 'TeamNotFound, - ErrorS 'InvalidTargetAccess - ] - HasConversationActionGalleyErrors 'ConversationResetTag = - '[ ErrorS (ActionDenied LeaveConversation), - ErrorS GroupIdVersionNotSupported, - ErrorS MLSStaleMessage, - ErrorS InvalidOperation, - ErrorS ConvNotFound - ] - HasConversationActionGalleyErrors 'ConversationHistoryUpdateTag = - '[ ErrorS (ActionDenied ModifyConversationAccess), - ErrorS GroupIdVersionNotSupported, - ErrorS HistoryNotSupported, - ErrorS MLSStaleMessage, - ErrorS InvalidOperation, - ErrorS ConvNotFound - ] + ensureAllowed _ action conv (ActorContext Nothing (Just tm)) = + case action of + ConversationJoin _ _ InternalAdd -> throwS @'ConvNotFound + ConversationJoin _ _ ExternalAdd -> ensureManageChannelsPermission conv tm + ensureAllowed loc action conv (ActorContext (Just origUser) _mTm) = + mapErrorS @'InvalidAction @('ActionDenied 'AddConversationMember) $ do + ensureConvRoleNotElevated origUser (role action) + checkGroupIdSupport loc conv action + ensureAllowed _ _ _ (ActorContext Nothing Nothing) = throwS @'ConvNotFound + + skipConversationRoleCheck conv = + \case + Nothing -> False + Just _ -> conv.metadata.cnvmChannelAddPermission == Just AddPermission.Everyone + + allowChannelManagePermission = True + +instance IsConversationAction 'ConversationLeaveTag where + type + HasConversationActionEffects 'ConversationLeaveTag r = + ( Member (Input ConversationSubsystemConfig) r, + Member BackendNotificationQueueAccess r, + Member ExternalAccess r, + Member E.ConversationStore r, + Member NotificationSubsystem r, + Member ProposalStore r, + Member Random r, + Member Now r, + Member (Error FederationError) r, + Member TinyLog r + ) + + type + HasConversationActionGalleyErrors 'ConversationLeaveTag = + '[ ErrorS ('ActionDenied 'LeaveConversation), + ErrorS 'InvalidOperation, + ErrorS 'ConvNotFound + ] + + performAction lconv qusr _conId () = do + leaveConversation qusr lconv + pure $ mkPerformActionResult () + + ensureAllowed _ _action _conv (ActorContext Nothing (Just _tm)) = + throwS @'ConvNotFound + ensureAllowed _loc _action _conv (ActorContext (Just _origUser) _mTm) = + pure () + ensureAllowed _ _ _ (ActorContext Nothing Nothing) = throwS @'ConvNotFound + +instance IsConversationAction 'ConversationRemoveMembersTag where + type + HasConversationActionEffects 'ConversationRemoveMembersTag r = + ( Member (Error NoChanges) r, + Member E.ConversationStore r, + Member (Input ConversationSubsystemConfig) r, + Member (Error FederationError) r, + Member TinyLog r, + Member BackendNotificationQueueAccess r, + Member ExternalAccess r, + Member ProposalStore r, + Member Now r, + Member Random r, + Member NotificationSubsystem r + ) + + type + HasConversationActionGalleyErrors 'ConversationRemoveMembersTag = + '[ ErrorS ('ActionDenied 'RemoveConversationMember), + ErrorS 'InvalidOperation, + ErrorS 'ConvNotFound + ] + + performAction lconv _qusr _conId action = do + let presentVictims = filter (isConvMemberL lconv) (toList . crmTargets $ action) + when (null presentVictims) noChanges + traverse_ (convDeleteMembers (toUserList lconv presentVictims)) lconv + -- send remove proposals in the MLS case + traverse_ (removeUser lconv RemoveUserExcludeMain) presentVictims + pure $ mkPerformActionResult action -- FUTUREWORK: should we return the filtered action here? + + ensureAllowed _ _action conv (ActorContext Nothing (Just tm)) = + ensureManageChannelsPermission conv tm + ensureAllowed _loc _action _conv (ActorContext (Just _origUser) _mTm) = + pure () + ensureAllowed _ _ _ (ActorContext Nothing Nothing) = throwS @'ConvNotFound + + allowChannelManagePermission = True + +instance IsConversationAction 'ConversationMemberUpdateTag where + type + HasConversationActionEffects 'ConversationMemberUpdateTag r = + ( Member (ErrorS 'ConvMemberNotFound) r, + Member E.ConversationStore r + ) + + type + HasConversationActionGalleyErrors 'ConversationMemberUpdateTag = + '[ ErrorS ('ActionDenied 'ModifyOtherConversationMember), + ErrorS 'InvalidOperation, + ErrorS 'ConvNotFound, + ErrorS 'ConvMemberNotFound + ] + + performAction lconv _qusr _conId action = do + let lcnv = fmap (.id_) lconv + storedConv = tUnqualified lconv + void $ ensureOtherMember lconv (cmuTarget action) storedConv + E.setOtherMember lcnv (cmuTarget action) (cmuUpdate action) + pure $ mkPerformActionResult action + + ensureAllowed _ _action _conv (ActorContext Nothing (Just _tm)) = + throwS @'ConvNotFound + ensureAllowed _loc _action _conv (ActorContext (Just _origUser) _mTm) = + pure () + ensureAllowed _ _ _ (ActorContext Nothing Nothing) = throwS @'ConvNotFound + + allowChannelManagePermission = True + +instance IsConversationAction 'ConversationDeleteTag where + type + HasConversationActionEffects 'ConversationDeleteTag r = + ( Member (ErrorS 'NotATeamMember) r, + Member E.ConversationStore r, + Member ProposalStore r, + Member CodeStore r + ) + + type + HasConversationActionGalleyErrors 'ConversationDeleteTag = + '[ ErrorS ('ActionDenied 'DeleteConversation), + ErrorS 'NotATeamMember, + ErrorS 'InvalidOperation, + ErrorS 'ConvNotFound + ] + + performAction lconv _qusr _conId () = do + let lcnv = fmap (.id_) lconv + storedConv = tUnqualified lconv + let deleteGroup groupId = do + E.removeAllMLSClients groupId + E.deleteAllProposals groupId + + let cid = storedConv.id_ + for_ (storedConv & mlsMetadata <&> cnvmlsGroupId . fst) $ \gidParent -> do + sconvs <- E.listSubConversations cid + for_ (Map.assocs sconvs) $ \(subid, mlsData) -> do + let gidSub = cnvmlsGroupId mlsData + E.deleteSubConversation cid subid + deleteGroup gidSub + deleteGroup gidParent + + key <- E.makeKey (tUnqualified lcnv) + E.deleteCode key + case convTeam storedConv of + Nothing -> E.deleteConversation (tUnqualified lcnv) + Just tid -> E.deleteTeamConversation tid (tUnqualified lcnv) + + pure $ mkPerformActionResult () + + ensureAllowed _ _action _conv (ActorContext Nothing (Just _tm)) = + throwS @'ConvNotFound + ensureAllowed loc _action conv (ActorContext (Just origUser) _mTm) = + for_ (convTeam conv) $ \tid -> do + lusr <- ensureLocal loc (convMemberId loc origUser) + void $ TeamSubsystem.internalGetTeamMember (tUnqualified lusr) tid >>= noteS @'NotATeamMember + ensureAllowed _ _ _ (ActorContext Nothing Nothing) = throwS @'ConvNotFound + + allowChannelManagePermission = True + +instance IsConversationAction 'ConversationRenameTag where + type + HasConversationActionEffects 'ConversationRenameTag r = + ( Member TeamSubsystem r, + Member (ErrorS InvalidOperation) r, + Member (Error InvalidInput) r, + Member E.ConversationStore r + ) + + type + HasConversationActionGalleyErrors 'ConversationRenameTag = + '[ ErrorS ('ActionDenied 'ModifyConversationName), + ErrorS 'InvalidOperation, + ErrorS 'ConvNotFound + ] + + performAction lconv qusr _conId action = do + let lcnv = fmap (.id_) lconv + storedConv = tUnqualified lconv + zusrMembership <- join <$> forM storedConv.metadata.cnvmTeam (TeamSubsystem.internalGetTeamMember (qUnqualified qusr)) + for_ zusrMembership $ \tm -> unless (tm `hasPermission` ModifyConvName) $ throwS @'InvalidOperation + cn <- rangeChecked (cupName action) + E.setConversationName (tUnqualified lcnv) cn + pure $ mkPerformActionResult action + + ensureAllowed _ _action conv (ActorContext Nothing (Just tm)) = + ensureManageChannelsPermission conv tm + ensureAllowed _loc _action _conv (ActorContext (Just _origUser) _mTm) = + pure () + ensureAllowed _ _ _ (ActorContext Nothing Nothing) = throwS @'ConvNotFound + + allowChannelManagePermission = True + +instance IsConversationAction 'ConversationAccessDataTag where + type + HasConversationActionEffects 'ConversationAccessDataTag r = + ( Member E.BrigAPIAccess r, + Member CodeStore r, + Member (Error NoChanges) r, + Member (ErrorS 'InvalidTargetAccess) r, + Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, + Member ExternalAccess r, + Member E.FireAndForget r, + Member NotificationSubsystem r, + Member (Input ConversationSubsystemConfig) r, + Member ProposalStore r, + Member TinyLog r, + Member Now r, + Member E.ConversationStore r, + Member Random r, + Member (Error FederationError) r, + Member BackendNotificationQueueAccess r, + Member ConversationSubsystem r, + Member TeamSubsystem r + ) + + type + HasConversationActionGalleyErrors 'ConversationAccessDataTag = + '[ ErrorS ('ActionDenied 'RemoveConversationMember), + ErrorS ('ActionDenied 'ModifyConversationAccess), + ErrorS 'InvalidOperation, + ErrorS 'InvalidTargetAccess, + ErrorS 'ConvNotFound + ] + + performAction lconv qusr _conId action = do + (bm, act) <- performConversationAccessData qusr lconv action + pure + PerformActionResult + { extraTargets = bm, + action = act, + extraConversationData = def + } + + ensureAllowed _ _action _conv (ActorContext Nothing (Just _tm)) = + throwS @'ConvNotFound + ensureAllowed _loc action conv (ActorContext (Just origUser) mTm) = do + -- 'PrivateAccessRole' is for self-conversations, 1:1 conversations and + -- so on; users not supposed to be able to make other conversations + -- have 'PrivateAccessRole' + when (PrivateAccess `elem` cupAccess action || Set.null (cupAccessRoles action)) $ + throwS @'InvalidTargetAccess + -- Team conversations incur another round of checks + case convTeam conv of + Just _ -> do + -- Access mode change might result in members being removed from the + -- conversation, so the user must have the necessary permission flag, + -- unless the actor is a team member with ManageChannels on a channel. + unless (maybe False (hasManageChannelsPermission conv) mTm) $ ensureActionAllowed SRemoveConversationMember origUser + Nothing -> + -- not a team conv, so one of the other access roles has to allow this. + when (Set.null $ cupAccessRoles action Set.\\ Set.fromList [TeamMemberAccessRole]) $ + throwS @'InvalidTargetAccess + ensureAllowed _ _ _ (ActorContext Nothing Nothing) = throwS @'ConvNotFound + + allowChannelManagePermission = True + +instance IsConversationAction 'ConversationHistoryUpdateTag where + type + HasConversationActionEffects 'ConversationHistoryUpdateTag r = + ( Member E.ConversationStore r, + Member (ErrorS HistoryNotSupported) r + ) + + type + HasConversationActionGalleyErrors 'ConversationHistoryUpdateTag = + '[ ErrorS (ActionDenied ModifyConversationAccess), + ErrorS HistoryNotSupported, + ErrorS InvalidOperation, + ErrorS ConvNotFound + ] + + performAction lconv _qusr _conId action = do + let lcnv = fmap (.id_) lconv + storedConv = tUnqualified lconv + when (storedConv.metadata.cnvmGroupConvType /= Just Channel) $ do + throwS @HistoryNotSupported + E.setConversationHistory (tUnqualified lcnv) action + pure $ mkPerformActionResult action + + ensureAllowed _ _action _conv (ActorContext Nothing (Just _tm)) = + throwS @'ConvNotFound + ensureAllowed _loc _action _conv (ActorContext (Just _origUser) _mTm) = + pure () + ensureAllowed _ _ _ (ActorContext Nothing Nothing) = throwS @'ConvNotFound + +instance IsConversationAction 'ConversationMessageTimerUpdateTag where + type + HasConversationActionEffects 'ConversationMessageTimerUpdateTag r = + ( Member E.ConversationStore r, + Member (Error NoChanges) r + ) + + type + HasConversationActionGalleyErrors 'ConversationMessageTimerUpdateTag = + '[ ErrorS ('ActionDenied 'ModifyConversationMessageTimer), + ErrorS 'InvalidOperation, + ErrorS 'ConvNotFound + ] + + performAction lconv _qusr _conId action = do + let lcnv = fmap (.id_) lconv + storedConv = tUnqualified lconv + when (Data.convMessageTimer storedConv == cupMessageTimer action) noChanges + E.setConversationMessageTimer (tUnqualified lcnv) (cupMessageTimer action) + pure $ mkPerformActionResult action + + ensureAllowed _ _action _conv (ActorContext Nothing (Just _)) = + throwS @'ConvNotFound + ensureAllowed _loc _action _conv (ActorContext (Just _) _) = + pure () + ensureAllowed _ _ _ (ActorContext Nothing Nothing) = throwS @'ConvNotFound + + allowChannelManagePermission = True + +instance IsConversationAction 'ConversationReceiptModeUpdateTag where + type + HasConversationActionEffects 'ConversationReceiptModeUpdateTag r = + ( Member (ErrorS MLSReadReceiptsNotAllowed) r, + Member E.ConversationStore r, + Member (Error NoChanges) r + ) + + type + HasConversationActionGalleyErrors 'ConversationReceiptModeUpdateTag = + '[ ErrorS ('ActionDenied 'ModifyConversationReceiptMode), + ErrorS 'InvalidOperation, + ErrorS 'MLSReadReceiptsNotAllowed, + ErrorS 'ConvNotFound + ] + + performAction lconv _qusr _conId action = do + let lcnv = fmap (.id_) lconv + storedConv = tUnqualified lconv + when (Data.convReceiptMode storedConv == Just (cruReceiptMode action)) noChanges + E.setConversationReceiptMode (tUnqualified lcnv) (cruReceiptMode action) + pure $ mkPerformActionResult action + + ensureAllowed _ _action _conv (ActorContext Nothing (Just _tm)) = + throwS @'ConvNotFound + ensureAllowed _loc _action conv (ActorContext (Just _origUser) _mTm) = do + -- cannot update receipt mode of MLS conversations + when (convProtocolTag conv == ProtocolMLSTag) $ + throwS @MLSReadReceiptsNotAllowed + ensureAllowed _ _ _ (ActorContext Nothing Nothing) = throwS @'ConvNotFound + +instance IsConversationAction 'ConversationUpdateProtocolTag where + type + HasConversationActionEffects 'ConversationUpdateProtocolTag r = + ( Member FeaturesConfigSubsystem r, + Member BackendNotificationQueueAccess r, + Member NotificationSubsystem r, + Member E.BrigAPIAccess r, + Member ExternalAccess r, + Member TinyLog r, + Member (Error NoChanges) r, + Member E.ConversationStore r, + Member Now r, + Member Random r, + Member (Input ConversationSubsystemConfig) r, + Member ProposalStore r, + Member (ErrorS ConvInvalidProtocolTransition) r, + Member (E.FederationAPIAccess FederatorClient) r, + Member (ErrorS MLSMigrationCriteriaNotSatisfied) r, + Member (Error FederationError) r + ) + + type + HasConversationActionGalleyErrors 'ConversationUpdateProtocolTag = + '[ ErrorS ('ActionDenied 'LeaveConversation), + ErrorS 'InvalidOperation, + ErrorS 'ConvNotFound, + ErrorS 'ConvInvalidProtocolTransition, + ErrorS 'MLSMigrationCriteriaNotSatisfied + ] + + performAction lconv qusr _conId action = do + let lcnv = fmap (.id_) lconv + storedConv = tUnqualified lconv + case (protocolTag (tUnqualified lconv).protocol, action, convTeam (tUnqualified lconv)) of + (ProtocolProteusTag, ProtocolMixedTag, Just _) -> do + let gid = Serialisation.newGroupId (convType (tUnqualified lconv)) $ Conv <$> tUntagged lcnv + epoch = Epoch 0 + E.updateToMixedProtocol (tUnqualified lcnv) gid epoch + pure $ mkPerformActionResult action + (ProtocolMixedTag, ProtocolMLSTag, Just tid) -> do + mig <- getFeatureForTeam tid + now <- Now.get + mlsConv <- mkMLSConversation storedConv >>= noteS @'ConvInvalidProtocolTransition + ok <- checkMigrationCriteria now mlsConv mig + unless ok $ throwS @'MLSMigrationCriteriaNotSatisfied + removeExtraneousClients qusr lconv + E.updateToMLSProtocol (tUnqualified lcnv) + pure $ mkPerformActionResult action + (ProtocolProteusTag, ProtocolProteusTag, _) -> + noChanges + (ProtocolMixedTag, ProtocolMixedTag, _) -> + noChanges + (ProtocolMLSTag, ProtocolMLSTag, _) -> + noChanges + (_, _, _) -> throwS @'ConvInvalidProtocolTransition + + ensureAllowed _ _action _conv (ActorContext Nothing (Just _tm)) = + throwS @'ConvNotFound + ensureAllowed _loc _action _conv (ActorContext (Just _origUser) _mTm) = + pure () + ensureAllowed _ _ _ (ActorContext Nothing Nothing) = throwS @'ConvNotFound + +instance IsConversationAction 'ConversationUpdateAddPermissionTag where + type + HasConversationActionEffects 'ConversationUpdateAddPermissionTag r = + ( Member (ErrorS 'InvalidTargetAccess) r, + Member (Error NoChanges) r, + Member E.ConversationStore r + ) + + type + HasConversationActionGalleyErrors 'ConversationUpdateAddPermissionTag = + '[ ErrorS ('ActionDenied 'ModifyAddPermission), + ErrorS 'InvalidOperation, + ErrorS 'ConvNotFound, + ErrorS 'InvalidTargetAccess + ] + + performAction lconv _qusr _conId action = do + let lcnv = fmap (.id_) lconv + storedConv = tUnqualified lconv + when (storedConv.metadata.cnvmChannelAddPermission == Just (addPermission action)) noChanges + E.updateChannelAddPermissions (tUnqualified lcnv) (addPermission action) + pure $ mkPerformActionResult action + + ensureAllowed _ _action _conv (ActorContext Nothing (Just _tm)) = + throwS @'ConvNotFound + ensureAllowed _loc _action conv (ActorContext (Just _origUser) _mTm) = do + unless (conv.metadata.cnvmGroupConvType == Just Channel) $ throwS @'InvalidTargetAccess + ensureAllowed _ _ _ (ActorContext Nothing Nothing) = throwS @'ConvNotFound + + allowChannelManagePermission = True + +instance IsConversationAction 'ConversationResetTag where + type + HasConversationActionEffects 'ConversationResetTag r = + ( Member BackendNotificationQueueAccess r, + Member (E.FederationAPIAccess FederatorClient) r, + Member ExternalAccess r, + Member ConversationSubsystem r, + Member E.ConversationStore r, + Member NotificationSubsystem r, + Member ProposalStore r, + Member E.MLSCommitLockStore r, + Member Resource r, + Member (Input ConversationSubsystemConfig) r, + Member (ErrorS MLSStaleMessage) r, + Member (ErrorS ConvNotFound) r, + Member (ErrorS InvalidOperation) r, + Member Random r, + Member Now r, + Member TinyLog r + ) + + type + HasConversationActionGalleyErrors 'ConversationResetTag = + '[ ErrorS (ActionDenied LeaveConversation), + ErrorS MLSStaleMessage, + ErrorS InvalidOperation, + ErrorS ConvNotFound + ] + + performAction lconv qusr _conId action = do + newGroupId <- resetLocalMLSMainConversation qusr lconv action + pure + PerformActionResult + { extraTargets = mempty, + action = action, + extraConversationData = ExtraConversationData (Just newGroupId) + } + + ensureAllowed _ _action _conv (ActorContext Nothing (Just _tm)) = + throwS @'ConvNotFound + ensureAllowed _loc _action _conv (ActorContext (Just _origUser) _mTm) = + pure () + ensureAllowed _ _ _ (ActorContext Nothing Nothing) = throwS @'ConvNotFound noChanges :: (Member (Error NoChanges) r) => Sem r a noChanges = throw NoChanges -ensureAllowed :: - forall tag mem r x. - ( IsConvMember mem, - HasConversationActionEffects tag r, - Member (ErrorS ConvNotFound) r, - Member (Error FederationError) r, - Member TeamSubsystem r - ) => - Sing tag -> - Local x -> - ConversationAction tag -> - StoredConversation -> - ActorContext mem -> - Sem r () -ensureAllowed tag _ action conv (ActorContext Nothing (Just tm)) = do - case tag of - SConversationRenameTag -> ensureManageChannelsPermission conv tm - SConversationJoinTag -> do - case action of - ConversationJoin _ _ InternalAdd -> throwS @'ConvNotFound - ConversationJoin _ _ ExternalAdd -> ensureManageChannelsPermission conv tm - SConversationRemoveMembersTag -> ensureManageChannelsPermission conv tm - _ -> throwS @'ConvNotFound -ensureAllowed tag loc action conv (ActorContext (Just origUser) mTm) = do - case tag of - SConversationJoinTag -> - mapErrorS @'InvalidAction @('ActionDenied 'AddConversationMember) $ do - ensureConvRoleNotElevated origUser (role action) - checkGroupIdSupport loc conv action - SConversationDeleteTag -> - for_ (convTeam conv) $ \tid -> do - lusr <- ensureLocal loc (convMemberId loc origUser) - 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 - -- have 'PrivateAccessRole' - when (PrivateAccess `elem` cupAccess action || Set.null (cupAccessRoles action)) $ - throwS @'InvalidTargetAccess - -- Team conversations incur another round of checks - case convTeam conv of - Just _ -> do - -- Access mode change might result in members being removed from the - -- conversation, so the user must have the necessary permission flag, - -- unless the actor is a team member with ManageChannels on a channel. - unless (maybe False (hasManageChannelsPermission conv) mTm) $ ensureActionAllowed SRemoveConversationMember origUser - Nothing -> - -- not a team conv, so one of the other access roles has to allow this. - when (Set.null $ cupAccessRoles action Set.\\ Set.fromList [TeamMemberAccessRole]) $ - throwS @'InvalidTargetAccess - SConversationUpdateAddPermissionTag -> do - unless (conv.metadata.cnvmGroupConvType == Just Channel) $ throwS @'InvalidTargetAccess - SConversationReceiptModeUpdateTag -> do - -- cannot update receipt mode of MLS conversations - when (convProtocolTag conv == ProtocolMLSTag) $ - throwS @MLSReadReceiptsNotAllowed - _ -> pure () -ensureAllowed _ _ _ _ (ActorContext Nothing Nothing) = throwS @'ConvNotFound - data PerformActionResult tag = PerformActionResult { extraTargets :: BotsAndMembers, @@ -451,144 +749,9 @@ mkPerformActionResult action = extraConversationData = def } --- | Returns additional members that resulted from the action (e.g. ConversationJoin) --- and also returns the (possible modified) action that was performed -performAction :: - forall tag r. - ( HasConversationActionEffects tag r, - Member BackendNotificationQueueAccess r, - Member TeamCollaboratorsSubsystem r, - Member (Error FederationError) r, - Member ConversationSubsystem r, - Member E.MLSCommitLockStore r, - Member TeamSubsystem r, - Member FederationSubsystem r, - Member (Input ConversationSubsystemConfig) r - ) => - Sing tag -> - Qualified UserId -> - Local StoredConversation -> - ConversationAction tag -> - Sem r (PerformActionResult tag) -performAction tag origUser lconv action = do - let lcnv = fmap (.id_) lconv - storedConv = tUnqualified lconv - case tag of - SConversationJoinTag -> do - (extraTargets, action') <- performConversationJoin origUser lconv action - pure - PerformActionResult - { extraTargets = extraTargets, - action = action', - extraConversationData = def - } - SConversationLeaveTag -> do - leaveConversation origUser lconv - pure $ mkPerformActionResult action - SConversationRemoveMembersTag -> do - let presentVictims = filter (isConvMemberL lconv) (toList . crmTargets $ action) - when (null presentVictims) noChanges - traverse_ (convDeleteMembers (toUserList lconv presentVictims)) lconv - -- send remove proposals in the MLS case - traverse_ (removeUser lconv RemoveUserExcludeMain) presentVictims - pure $ mkPerformActionResult action -- FUTUREWORK: should we return the filtered action here? - SConversationMemberUpdateTag -> do - void $ ensureOtherMember lconv (cmuTarget action) storedConv - E.setOtherMember lcnv (cmuTarget action) (cmuUpdate action) - pure $ mkPerformActionResult action - SConversationDeleteTag -> do - let deleteGroup groupId = do - E.removeAllMLSClients groupId - E.deleteAllProposals groupId - - let cid = storedConv.id_ - for_ (storedConv & mlsMetadata <&> cnvmlsGroupId . fst) $ \gidParent -> do - sconvs <- E.listSubConversations cid - for_ (Map.assocs sconvs) $ \(subid, mlsData) -> do - let gidSub = cnvmlsGroupId mlsData - E.deleteSubConversation cid subid - deleteGroup gidSub - deleteGroup gidParent - - key <- E.makeKey (tUnqualified lcnv) - E.deleteCode key - case convTeam storedConv of - Nothing -> E.deleteConversation (tUnqualified lcnv) - Just tid -> E.deleteTeamConversation tid (tUnqualified lcnv) - - pure $ mkPerformActionResult action - SConversationRenameTag -> do - zusrMembership <- join <$> forM storedConv.metadata.cnvmTeam (TeamSubsystem.internalGetTeamMember (qUnqualified origUser)) - for_ zusrMembership $ \tm -> unless (tm `hasPermission` ModifyConvName) $ throwS @'InvalidOperation - cn <- rangeChecked (cupName action) - E.setConversationName (tUnqualified lcnv) cn - pure $ mkPerformActionResult action - SConversationMessageTimerUpdateTag -> do - when (Data.convMessageTimer storedConv == cupMessageTimer action) noChanges - E.setConversationMessageTimer (tUnqualified lcnv) (cupMessageTimer action) - pure $ mkPerformActionResult action - SConversationReceiptModeUpdateTag -> do - when (Data.convReceiptMode storedConv == Just (cruReceiptMode action)) noChanges - E.setConversationReceiptMode (tUnqualified lcnv) (cruReceiptMode action) - pure $ mkPerformActionResult action - SConversationAccessDataTag -> do - (bm, act) <- performConversationAccessData origUser lconv action - pure - PerformActionResult - { extraTargets = bm, - action = act, - extraConversationData = def - } - SConversationUpdateProtocolTag -> do - case (protocolTag (tUnqualified lconv).protocol, action, convTeam (tUnqualified lconv)) of - (ProtocolProteusTag, ProtocolMixedTag, Just _) -> do - let gid = Serialisation.newGroupId (convType (tUnqualified lconv)) $ Conv <$> tUntagged lcnv - epoch = Epoch 0 - E.updateToMixedProtocol (tUnqualified lcnv) gid epoch - pure $ mkPerformActionResult action - (ProtocolMixedTag, ProtocolMLSTag, Just tid) -> do - mig <- getFeatureForTeam tid - now <- Now.get - mlsConv <- mkMLSConversation storedConv >>= noteS @'ConvInvalidProtocolTransition - ok <- checkMigrationCriteria now mlsConv mig - unless ok $ throwS @'MLSMigrationCriteriaNotSatisfied - removeExtraneousClients origUser lconv - E.updateToMLSProtocol (tUnqualified lcnv) - pure $ mkPerformActionResult action - (ProtocolProteusTag, ProtocolProteusTag, _) -> - noChanges - (ProtocolMixedTag, ProtocolMixedTag, _) -> - noChanges - (ProtocolMLSTag, ProtocolMLSTag, _) -> - noChanges - (_, _, _) -> throwS @'ConvInvalidProtocolTransition - SConversationUpdateAddPermissionTag -> do - when (storedConv.metadata.cnvmChannelAddPermission == Just (addPermission action)) noChanges - E.updateChannelAddPermissions (tUnqualified lcnv) (addPermission action) - pure $ mkPerformActionResult action - SConversationResetTag -> do - newGroupId <- resetLocalMLSMainConversation origUser lconv action - pure - PerformActionResult - { extraTargets = mempty, - action = action, - extraConversationData = ExtraConversationData (Just newGroupId) - } - SConversationHistoryUpdateTag -> do - when (storedConv.metadata.cnvmGroupConvType /= Just Channel) $ do - throwS @HistoryNotSupported - E.setConversationHistory (tUnqualified lcnv) action - pure $ mkPerformActionResult action - performConversationJoin :: forall r. - ( HasConversationActionEffects 'ConversationJoinTag r, - Member BackendNotificationQueueAccess r, - Member ConversationSubsystem r, - Member TeamCollaboratorsSubsystem r, - Member FederationSubsystem r, - Member TeamSubsystem r - ) => + (HasConversationActionEffects 'ConversationJoinTag r) => Qualified UserId -> Local StoredConversation -> ConversationJoin -> @@ -738,12 +901,7 @@ performConversationJoin qusr lconv (ConversationJoin invited role joinType) = do Nothing -> pure () performConversationAccessData :: - ( HasConversationActionEffects 'ConversationAccessDataTag r, - Member (Error FederationError) r, - Member BackendNotificationQueueAccess r, - Member ConversationSubsystem r, - Member TeamSubsystem r - ) => + (HasConversationActionEffects 'ConversationAccessDataTag r) => Qualified UserId -> Local StoredConversation -> ConversationAccessData -> @@ -791,7 +949,7 @@ performConversationAccessData qusr lconv action = do then pure bm else pure $ bm {bmBots = mempty} - maybeRemoveGuests :: (Member BrigAPIAccess r) => BotsAndMembers -> Sem r BotsAndMembers + maybeRemoveGuests :: (Member E.BrigAPIAccess r) => BotsAndMembers -> Sem r BotsAndMembers maybeRemoveGuests bm = if Set.member GuestAccessRole (cupAccessRoles action) then pure bm @@ -820,22 +978,392 @@ performConversationAccessData qusr lconv action = do pure $ bm {bmLocals = Set.fromList noTeamMembers} Nothing -> pure bm -updateLocalConversation :: - forall tag r. +updateLocalConversationJoin :: ( Member BackendNotificationQueueAccess r, - Member ConversationStore r, Member (Error FederationError) r, - Member (ErrorS ('ActionDenied (ConversationActionPermission tag))) r, + Member (ErrorS ('ActionDenied (ConversationActionPermission 'ConversationJoinTag))) r, Member (ErrorS 'InvalidOperation) r, Member (ErrorS 'ConvNotFound) r, Member ConversationSubsystem r, - HasConversationActionEffects tag r, - SingI tag, Member FederationSubsystem r, Member TeamCollaboratorsSubsystem r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r, + Member E.BrigAPIAccess r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS 'NotConnected) r, + Member (ErrorS 'ConvAccessDenied) r, + Member (ErrorS 'TooManyMembers) r, + Member (ErrorS 'MissingLegalholdConsent) r, + Member (ErrorS 'GroupIdVersionNotSupported) r, + Member (Error UnreachableBackends) r, + Member ExternalAccess r, + Member (E.FederationAPIAccess FederatorClient) r, + Member NotificationSubsystem r, + Member Now r, + Member LegalHoldStore r, + Member ProposalStore r, + Member Random r, + Member TeamStore r, + Member TinyLog r, + Member E.ConversationStore r, + Member (Error NoChanges) r + ) => + Local ConvId -> + Qualified UserId -> + Maybe ConnId -> + ConversationJoin -> + Sem r LocalConversationUpdate +updateLocalConversationJoin = + updateLocalConversation @'ConversationJoinTag + +updateLocalConversationLeave :: + ( Member BackendNotificationQueueAccess r, + Member (Error FederationError) r, + Member (ErrorS ('ActionDenied (ConversationActionPermission 'ConversationLeaveTag))) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'ConvNotFound) r, + Member ConversationSubsystem r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r, + Member ExternalAccess r, + Member NotificationSubsystem r, + Member Now r, + Member ProposalStore r, + Member E.ConversationStore r, + Member Random r, + Member TinyLog r + ) => + Local ConvId -> + Qualified UserId -> + Maybe ConnId -> + Sem r LocalConversationUpdate +updateLocalConversationLeave lcnvId qusr connId = + updateLocalConversation @'ConversationLeaveTag lcnvId qusr connId () + +updateLocalConversationMemberUpdate :: + ( Member (Error FederationError) r, + Member (ErrorS ('ActionDenied (ConversationActionPermission 'ConversationMemberUpdateTag))) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'ConvNotFound) r, + Member ConversationSubsystem r, + Member TeamSubsystem r, + Member (ErrorS ConvMemberNotFound) r, + Member E.ConversationStore r + ) => + Local ConvId -> + Qualified UserId -> + Maybe ConnId -> + ConversationMemberUpdate -> + Sem r LocalConversationUpdate +updateLocalConversationMemberUpdate = + updateLocalConversation @'ConversationMemberUpdateTag + +updateLocalConversationDelete :: + ( Member (ErrorS ('ActionDenied (ConversationActionPermission 'ConversationDeleteTag))) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'ConvNotFound) r, + Member ConversationSubsystem r, + Member TeamSubsystem r, + Member CodeStore r, + Member E.ConversationStore r, + Member (Error FederationError) r, + Member (ErrorS 'NotATeamMember) r, + Member ProposalStore r + ) => + Local ConvId -> + Qualified UserId -> + Maybe ConnId -> + Sem r LocalConversationUpdate +updateLocalConversationDelete lcnvId uid connId = + updateLocalConversation @'ConversationDeleteTag lcnvId uid connId () + +updateLocalConversationRename :: + ( Member (Error FederationError) r, + Member (ErrorS ('ActionDenied (ConversationActionPermission 'ConversationRenameTag))) r, + Member (ErrorS 'ConvNotFound) r, + Member ConversationSubsystem r, + Member TeamSubsystem r, + Member (Error InvalidInput) r, + Member E.ConversationStore r, + Member (ErrorS InvalidOperation) r + ) => + Local ConvId -> + Qualified UserId -> + Maybe ConnId -> + ConversationRename -> + Sem r LocalConversationUpdate +updateLocalConversationRename = + updateLocalConversation @'ConversationRenameTag + +updateLocalConversationMessageTimerUpdate :: + ( Member (Error FederationError) r, + Member (ErrorS ('ActionDenied (ConversationActionPermission 'ConversationMessageTimerUpdateTag))) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'ConvNotFound) r, + Member ConversationSubsystem r, + Member TeamSubsystem r, + Member E.ConversationStore r, + Member (Error NoChanges) r + ) => + Local ConvId -> + Qualified UserId -> + Maybe ConnId -> + ConversationMessageTimerUpdate -> + Sem r LocalConversationUpdate +updateLocalConversationMessageTimerUpdate = + updateLocalConversation @'ConversationMessageTimerUpdateTag + +updateLocalConversationReceiptModeUpdate :: + ( Member (Error FederationError) r, + Member (ErrorS ('ActionDenied (ConversationActionPermission 'ConversationReceiptModeUpdateTag))) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'ConvNotFound) r, + Member ConversationSubsystem r, + Member TeamSubsystem r, + Member E.ConversationStore r, + Member (Error NoChanges) r, + Member (ErrorS MLSReadReceiptsNotAllowed) r + ) => + Local ConvId -> + Qualified UserId -> + Maybe ConnId -> + ConversationReceiptModeUpdate -> + Sem r LocalConversationUpdate +updateLocalConversationReceiptModeUpdate = + updateLocalConversation @'ConversationReceiptModeUpdateTag + +updateLocalConversationAccessData :: + ( Member BackendNotificationQueueAccess r, + Member (Error FederationError) r, + Member (ErrorS ('ActionDenied (ConversationActionPermission 'ConversationAccessDataTag))) r, + Member (ErrorS 'ConvNotFound) r, + Member ConversationSubsystem r, + Member (Error NoChanges) r, + Member TinyLog r, + Member E.ConversationStore r, + Member E.BrigAPIAccess r, + Member CodeStore r, + Member ExternalAccess r, + Member NotificationSubsystem r, + Member ProposalStore r, + Member Now r, + Member Random r, + Member E.FireAndForget r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r, + Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'InvalidTargetAccess) r + ) => + Local ConvId -> + Qualified UserId -> + Maybe ConnId -> + ConversationAccessData -> + Sem r LocalConversationUpdate +updateLocalConversationAccessData = + updateLocalConversation @'ConversationAccessDataTag + +updateLocalConversationRemoveMembers :: + ( Member BackendNotificationQueueAccess r, + Member (Error FederationError) r, + Member (ErrorS ('ActionDenied (ConversationActionPermission 'ConversationRemoveMembersTag))) r, + Member ConversationSubsystem r, + Member (Error NoChanges) r, + Member TinyLog r, + Member E.ConversationStore r, + Member ExternalAccess r, + Member NotificationSubsystem r, + Member ProposalStore r, + Member Now r, + Member Random r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'ConvNotFound) r + ) => + Local ConvId -> + Qualified UserId -> + Maybe ConnId -> + ConversationRemoveMembers -> + Sem r LocalConversationUpdate +updateLocalConversationRemoveMembers = + updateLocalConversation @'ConversationRemoveMembersTag + +updateLocalConversationUpdateProtocol :: + ( Member BackendNotificationQueueAccess r, + Member (Error FederationError) r, + Member (ErrorS ('ActionDenied (ConversationActionPermission 'ConversationUpdateProtocolTag))) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'ConvNotFound) r, + Member ConversationSubsystem r, + Member (Error NoChanges) r, + Member (E.FederationAPIAccess FederatorClient) r, + Member TinyLog r, + Member E.ConversationStore r, + Member E.BrigAPIAccess r, + Member ExternalAccess r, + Member NotificationSubsystem r, + Member ProposalStore r, + Member Now r, + Member Random r, + Member TeamSubsystem r, + Member FeaturesConfigSubsystem r, + Member (Input ConversationSubsystemConfig) r, + Member (ErrorS 'ConvInvalidProtocolTransition) r, + Member (ErrorS 'MLSMigrationCriteriaNotSatisfied) r + ) => + Local ConvId -> + Qualified UserId -> + Maybe ConnId -> + ProtocolTag -> + Sem r LocalConversationUpdate +updateLocalConversationUpdateProtocol = + updateLocalConversation @'ConversationUpdateProtocolTag + +updateLocalConversationUpdateAddPermission :: + ( Member (Error FederationError) r, + Member (ErrorS ('ActionDenied (ConversationActionPermission 'ConversationUpdateAddPermissionTag))) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'ConvNotFound) r, + Member ConversationSubsystem r, + Member (Error NoChanges) r, + Member E.ConversationStore r, + Member TeamSubsystem r, + Member (ErrorS 'InvalidTargetAccess) r + ) => + Local ConvId -> + Qualified UserId -> + Maybe ConnId -> + AddPermissionUpdate -> + Sem r LocalConversationUpdate +updateLocalConversationUpdateAddPermission = + updateLocalConversation @'ConversationUpdateAddPermissionTag + +updateLocalConversationReset :: + ( Member BackendNotificationQueueAccess r, + Member (Error FederationError) r, + Member (ErrorS ('ActionDenied (ConversationActionPermission 'ConversationResetTag))) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'ConvNotFound) r, + Member ConversationSubsystem r, + Member (E.FederationAPIAccess FederatorClient) r, + Member TinyLog r, + Member E.ConversationStore r, + Member ExternalAccess r, + Member NotificationSubsystem r, + Member ProposalStore r, + Member Now r, + Member Random r, + Member Resource r, Member E.MLSCommitLockStore r, Member TeamSubsystem r, - Member (Input ConversationSubsystemConfig) r + Member (Input ConversationSubsystemConfig) r, + Member (ErrorS MLSStaleMessage) r + ) => + Local ConvId -> + Qualified UserId -> + Maybe ConnId -> + MLSReset -> + Sem r LocalConversationUpdate +updateLocalConversationReset = + updateLocalConversation @'ConversationResetTag + +updateLocalConversationHistoryUpdate :: + ( Member (Error FederationError) r, + Member (ErrorS ('ActionDenied (ConversationActionPermission 'ConversationHistoryUpdateTag))) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'ConvNotFound) r, + Member ConversationSubsystem r, + Member E.ConversationStore r, + Member TeamSubsystem r, + Member (ErrorS HistoryNotSupported) r + ) => + Local ConvId -> + Qualified UserId -> + Maybe ConnId -> + History -> + Sem r LocalConversationUpdate +updateLocalConversationHistoryUpdate = + updateLocalConversation @'ConversationHistoryUpdateTag + +updateLocalConversationUncheckedJoin :: + ( Member BackendNotificationQueueAccess r, + Member (Error FederationError) r, + Member (ErrorS ('ActionDenied (ConversationActionPermission 'ConversationJoinTag))) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'ConvNotFound) r, + Member ConversationSubsystem r, + Member FederationSubsystem r, + Member TeamCollaboratorsSubsystem r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r, + Member E.BrigAPIAccess r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS 'NotConnected) r, + Member (ErrorS 'ConvAccessDenied) r, + Member (ErrorS 'TooManyMembers) r, + Member (ErrorS 'MissingLegalholdConsent) r, + Member (ErrorS 'GroupIdVersionNotSupported) r, + Member (Error UnreachableBackends) r, + Member ExternalAccess r, + Member (E.FederationAPIAccess FederatorClient) r, + Member NotificationSubsystem r, + Member Now r, + Member LegalHoldStore r, + Member ProposalStore r, + Member Random r, + Member TeamStore r, + Member TinyLog r, + Member E.ConversationStore r, + Member (Error NoChanges) r + ) => + Local StoredConversation -> + Qualified UserId -> + Maybe ConnId -> + ConversationJoin -> + Sem r LocalConversationUpdate +updateLocalConversationUncheckedJoin = + updateLocalConversationUnchecked @'ConversationJoinTag + +updateLocalConversationUncheckedRemoveMembers :: + ( Member BackendNotificationQueueAccess r, + Member (Error FederationError) r, + Member (ErrorS ('ActionDenied (ConversationActionPermission 'ConversationRemoveMembersTag))) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'ConvNotFound) r, + Member ConversationSubsystem r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r, + Member (Error NoChanges) r, + Member E.ConversationStore r, + Member ProposalStore r, + Member Now r, + Member ExternalAccess r, + Member NotificationSubsystem r, + Member Random r, + Member TinyLog r + ) => + Local StoredConversation -> + Qualified UserId -> + Maybe ConnId -> + ConversationRemoveMembers -> + Sem r LocalConversationUpdate +updateLocalConversationUncheckedRemoveMembers = + updateLocalConversationUnchecked @'ConversationRemoveMembersTag + +updateLocalConversation :: + forall tag r. + ( Member E.ConversationStore r, + Member (Error FederationError) r, + Member (ErrorS ('ActionDenied (ConversationActionPermission tag))) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'ConvNotFound) r, + Member ConversationSubsystem r, + HasConversationActionEffects tag r, + IsConversationAction tag, + SingI tag, + Member TeamSubsystem r ) => Local ConvId -> Qualified UserId -> @@ -861,18 +1389,14 @@ updateLocalConversation lcnv qusr con action = do updateLocalConversationUnchecked :: forall tag r. ( SingI tag, - Member BackendNotificationQueueAccess r, + IsConversationAction tag, Member (Error FederationError) r, Member (ErrorS ('ActionDenied (ConversationActionPermission tag))) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member ConversationSubsystem r, HasConversationActionEffects tag r, - Member TeamCollaboratorsSubsystem r, - Member FederationSubsystem r, - Member E.MLSCommitLockStore r, - Member TeamSubsystem r, - Member (Input ConversationSubsystemConfig) r + Member TeamSubsystem r ) => Local StoredConversation -> Qualified UserId -> @@ -884,7 +1408,7 @@ updateLocalConversationUnchecked lconv qusr con action = do conv = tUnqualified lconv mTeamMember <- foldQualified lconv (getTeamMembership conv) (const $ pure Nothing) qusr ensureConversationActionAllowed (sing @tag) lcnv conv mTeamMember - par <- performAction (sing @tag) qusr lconv action + par <- performAction @tag lconv qusr con action sendConversationActionNotifications (sing @tag) qusr @@ -906,32 +1430,13 @@ updateLocalConversationUnchecked lconv qusr con action = do -- permission unless we intentionally skip it (channel overrides or -- special join case). unless - (skipConversationRoleCheck tag conv mTeamMember || (hasChannelManagePerm && channelAdminOverride tag)) + (skipConversationRoleCheck @tag conv mTeamMember || (hasChannelManagePerm && allowChannelManagePermission @tag)) (for_ mMem (ensureActionAllowed (sConversationActionPermission tag))) checkConversationType (fromSing tag) conv -- extra action-specific checks - ensureAllowed tag loc action conv (ActorContext mMem mTeamMember) - - skipConversationRoleCheck :: Sing tag -> StoredConversation -> Maybe TeamMember -> Bool - skipConversationRoleCheck SConversationJoinTag conv (Just _) = conv.metadata.cnvmChannelAddPermission == Just AddPermission.Everyone - skipConversationRoleCheck _ _ _ = False - - -- channelAdminOverride is necessary to let team admins act as "channel admins" even if their conversation_role isn't wire_admin, - -- but only for the intended actions. It’s placed here so we bypass only the generic role check and still enforce - -- all channel- and protocol-specific rules afterwards. - channelAdminOverride :: Sing tag -> Bool - channelAdminOverride = \case - SConversationJoinTag -> True - SConversationRemoveMembersTag -> True - SConversationMemberUpdateTag -> True - SConversationRenameTag -> True - SConversationMessageTimerUpdateTag -> True - SConversationAccessDataTag -> True - SConversationUpdateAddPermissionTag -> True - SConversationDeleteTag -> True - _ -> False + ensureAllowed @tag loc action conv (ActorContext mMem mTeamMember) -- -------------------------------------------------------------------------------- -- -- Utilities @@ -940,7 +1445,7 @@ updateLocalConversationUnchecked lconv qusr con action = do -- notification targets and the action performed. addMembersToLocalConversation :: ( Member (Error NoChanges) r, - Member ConversationStore r + Member E.ConversationStore r ) => Local ConvId -> UserList UserId -> @@ -953,7 +1458,7 @@ addMembersToLocalConversation lcnv users role joinType = do let action = ConversationJoin neUsers role joinType pure (bmFromMembers lmems rmems, action) -setOutOfSyncFlag :: (Member ConversationStore r) => Local StoredConversation -> UserList UserId -> Sem r () +setOutOfSyncFlag :: (Member E.ConversationStore r) => Local StoredConversation -> UserList UserId -> Sem r () setOutOfSyncFlag (tUnqualified -> conv) newMembers = let goingOutOfSync | ulNull newMembers = False @@ -968,11 +1473,11 @@ setOutOfSyncFlag (tUnqualified -> conv) newMembers = -- | Update the local database with information on conversation members joining -- or leaving. Finally, push out notifications to local users. updateLocalStateOfRemoteConv :: - ( Member BrigAPIAccess r, + ( Member E.BrigAPIAccess r, Member NotificationSubsystem r, Member ExternalAccess r, Member (Input (Local ())) r, - Member ConversationStore r, + Member E.ConversationStore r, Member P.TinyLog r ) => Remote F.ConversationUpdate -> @@ -1057,8 +1562,8 @@ updateLocalStateOfRemoteConv rcu con = do pushConversationEvent con () event (qualifyAs loc targets) [] $> event addLocalUsersToRemoteConv :: - ( Member BrigAPIAccess r, - Member ConversationStore r, + ( Member E.BrigAPIAccess r, + Member E.ConversationStore r, Member P.TinyLog r ) => Remote ConvId -> @@ -1097,7 +1602,7 @@ notifyTypingIndicator :: ( Member Now r, Member (Input (Local ())) r, Member NotificationSubsystem r, - Member (FederationAPIAccess FederatorClient) r + Member (E.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 4e9d09487be..7356b04c10b 100644 --- a/services/galley/src/Galley/API/Action/Kick.hs +++ b/services/galley/src/Galley/API/Action/Kick.hs @@ -23,7 +23,6 @@ import Data.Qualified import Data.Singletons import Galley.API.Action.Leave import Galley.API.Action.Notify -import Galley.Effects import Imports hiding ((\\)) import Polysemy import Polysemy.Error @@ -34,10 +33,15 @@ import Wire.API.Conversation.Action import Wire.API.Conversation.Config (ConversationSubsystemConfig) import Wire.API.Event.LeaveReason import Wire.API.Federation.Error +import Wire.BackendNotificationQueueAccess +import Wire.ConversationStore (ConversationStore) import Wire.ConversationSubsystem import Wire.ConversationSubsystem.Util +import Wire.ExternalAccess import Wire.NotificationSubsystem +import Wire.ProposalStore (ProposalStore) import Wire.Sem.Now (Now) +import Wire.Sem.Random (Random) import Wire.StoredConversation -- | Kick a user from a conversation and send notifications. diff --git a/services/galley/src/Galley/API/Action/Leave.hs b/services/galley/src/Galley/API/Action/Leave.hs index a4e397a92e5..0141cc4ad2c 100644 --- a/services/galley/src/Galley/API/Action/Leave.hs +++ b/services/galley/src/Galley/API/Action/Leave.hs @@ -21,7 +21,6 @@ import Control.Lens import Data.Id import Data.Qualified import Galley.API.MLS.Removal -import Galley.Effects import Imports hiding ((\\)) import Polysemy import Polysemy.Error @@ -29,9 +28,14 @@ import Polysemy.Input import Polysemy.TinyLog import Wire.API.Conversation.Config (ConversationSubsystemConfig) import Wire.API.Federation.Error +import Wire.BackendNotificationQueueAccess +import Wire.ConversationStore (ConversationStore) import Wire.ConversationSubsystem.Util +import Wire.ExternalAccess import Wire.NotificationSubsystem +import Wire.ProposalStore (ProposalStore) import Wire.Sem.Now (Now) +import Wire.Sem.Random (Random) import Wire.StoredConversation import Wire.UserList diff --git a/services/galley/src/Galley/API/Action/Notify.hs b/services/galley/src/Galley/API/Action/Notify.hs index 6ede2e2cc22..77c8167ebc3 100644 --- a/services/galley/src/Galley/API/Action/Notify.hs +++ b/services/galley/src/Galley/API/Action/Notify.hs @@ -20,7 +20,6 @@ module Galley.API.Action.Notify where import Data.Id import Data.Qualified import Data.Singletons -import Galley.Effects import Imports hiding ((\\)) import Polysemy import Wire.API.Conversation hiding (Conversation, Member) diff --git a/services/galley/src/Galley/API/Action/Reset.hs b/services/galley/src/Galley/API/Action/Reset.hs index 492146ac15a..49614ab300d 100644 --- a/services/galley/src/Galley/API/Action/Reset.hs +++ b/services/galley/src/Galley/API/Action/Reset.hs @@ -24,7 +24,6 @@ import Data.Id import Data.Qualified import Galley.API.Action.Kick import Galley.API.MLS.Util -import Galley.Effects import Imports import Polysemy import Polysemy.Error @@ -46,12 +45,16 @@ import Wire.API.MLS.Group.Serialisation qualified as Group import Wire.API.MLS.SubConversation import Wire.API.Routes.Public.Galley.MLS import Wire.API.VersionInfo +import Wire.BackendNotificationQueueAccess import Wire.ConversationStore import Wire.ConversationSubsystem import Wire.ConversationSubsystem.Util +import Wire.ExternalAccess import Wire.FederationAPIAccess import Wire.NotificationSubsystem +import Wire.ProposalStore (ProposalStore) import Wire.Sem.Now (Now) +import Wire.Sem.Random (Random) import Wire.StoredConversation as Data resetLocalMLSMainConversation :: diff --git a/services/galley/src/Galley/API/Clients.hs b/services/galley/src/Galley/API/Clients.hs index ef167dcf500..70af95788cf 100644 --- a/services/galley/src/Galley/API/Clients.hs +++ b/services/galley/src/Galley/API/Clients.hs @@ -27,8 +27,6 @@ import Data.Qualified import Data.Range import Galley.API.MLS.Removal import Galley.API.Query qualified as Query -import Galley.Effects -import Galley.Env import Galley.Types.Clients (clientIds) import Galley.Types.Error import Imports @@ -43,13 +41,17 @@ import Wire.API.Conversation.Config (ConversationSubsystemConfig) import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Error +import Wire.API.MLS.Keys (MLSKeysByPurpose, MLSPrivateKeys) import Wire.API.Routes.MultiTablePaging import Wire.BackendNotificationQueueAccess -import Wire.ConversationStore (getConversation) +import Wire.ConversationStore (ConversationStore, getConversation) import Wire.ConversationSubsystem qualified as ConvSubsystem import Wire.ConversationSubsystem.Util +import Wire.ExternalAccess (ExternalAccess) import Wire.NotificationSubsystem +import Wire.ProposalStore (ProposalStore) import Wire.Sem.Now (Now) +import Wire.Sem.Random (Random) import Wire.UserClientIndexStore qualified as E getClients :: @@ -63,13 +65,14 @@ getClients usr = clientIds usr <$> ConvSubsystem.internalGetClientIds [usr] -- the "clients" table in Galley. rmClient :: forall r. - ( Member UserClientIndexStore r, + ( Member E.UserClientIndexStore r, Member ConversationStore r, + Member ConvSubsystem.ConversationSubsystem r, Member (Error FederationError) r, Member ExternalAccess r, Member BackendNotificationQueueAccess r, Member NotificationSubsystem r, - Member (Input Env) r, + Member (Input (Maybe (MLSKeysByPurpose MLSPrivateKeys))) r, Member (Input (Local ())) r, Member Now r, Member (Error InternalError) r, diff --git a/services/galley/src/Galley/API/CustomBackend.hs b/services/galley/src/Galley/API/CustomBackend.hs index 1e82482a22b..74372356386 100644 --- a/services/galley/src/Galley/API/CustomBackend.hs +++ b/services/galley/src/Galley/API/CustomBackend.hs @@ -22,14 +22,12 @@ module Galley.API.CustomBackend where import Data.Domain (Domain) -import Galley.Effects.CustomBackendStore import Imports hiding ((\\)) import Polysemy import Wire.API.CustomBackend qualified as Public import Wire.API.Error import Wire.API.Error.Galley - --- PUBLIC --------------------------------------------------------------------- +import Wire.CustomBackendStore getCustomBackendByDomain :: ( Member CustomBackendStore r, diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index f5daa7ff3d5..560000626df 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -1,6 +1,3 @@ -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RecordWildCards #-} - -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -20,94 +17,15 @@ module Galley.API.Federation where -import Control.Error hiding (note) -import Control.Lens -import Data.Bifunctor -import Data.ByteString.Conversion (toByteString') -import Data.Default -import Data.Domain (Domain) -import Data.Id -import Data.Json.Util -import Data.Map qualified as Map -import Data.Map.Lens (toMapOf) -import Data.Qualified -import Data.Range (Range (fromRange)) -import Data.Set qualified as Set -import Data.Singletons (SingI (..), demote, sing) -import Data.Tagged -import Data.Text.Lazy qualified as LT -import Galley.API.Action -import Galley.API.MLS -import Galley.API.MLS.Enabled -import Galley.API.MLS.GroupInfo -import Galley.API.MLS.Message -import Galley.API.MLS.One2One -import Galley.API.MLS.Removal -import Galley.API.MLS.SubConversation hiding (leaveSubConversation) -import Galley.API.MLS.Util -import Galley.API.MLS.Welcome -import Galley.API.Mapping -import Galley.API.Mapping qualified as Mapping -import Galley.API.Message -import Galley.API.Push +import Galley.API.Federation.Handlers import Galley.App -import Galley.Effects -import Galley.Options -import Galley.Types.Conversations.One2One -import Galley.Types.Error -import Imports -import Network.Wai.Utilities.Exception import Polysemy -import Polysemy.Error -import Polysemy.Input -import Polysemy.Internal.Kind (Append) -import Polysemy.Resource -import Polysemy.TinyLog -import Polysemy.TinyLog qualified as P import Servant (ServerT) import Servant.API -import System.Logger.Class qualified as Log -import Wire.API.Conversation hiding (Member) -import Wire.API.Conversation qualified as Public -import Wire.API.Conversation.Action -import Wire.API.Conversation.Config (ConversationSubsystemConfig) -import Wire.API.Conversation.Role -import Wire.API.Error -import Wire.API.Error.Galley -import Wire.API.Event.Conversation import Wire.API.Federation.API -import Wire.API.Federation.API.Common (EmptyResponse (..)) -import Wire.API.Federation.API.Galley hiding (id) -import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Endpoint -import Wire.API.Federation.Error import Wire.API.Federation.Version -import Wire.API.MLS.Credential -import Wire.API.MLS.GroupInfo -import Wire.API.MLS.Keys -import Wire.API.MLS.Serialisation -import Wire.API.MLS.SubConversation -import Wire.API.Message -import Wire.API.Push.V2 (RecipientClients (..)) import Wire.API.Routes.Named -import Wire.API.Routes.Public.Galley.MLS -import Wire.API.ServantProto -import Wire.API.User (BaseProtocolTag (..)) -import Wire.CodeStore -import Wire.ConversationStore qualified as E -import Wire.ConversationSubsystem -import Wire.ConversationSubsystem.Util -import Wire.FeaturesConfigSubsystem -import Wire.FederationSubsystem (FederationSubsystem) -import Wire.FireAndForget qualified as E -import Wire.NotificationSubsystem -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.UserList (UserList (UserList)) type FederationAPI = "federation" :> FedApi 'Galley @@ -138,907 +56,3 @@ federationSitemap = :<|> Named @(Versioned 'V0 "on-conversation-updated") onConversationUpdatedV0 :<|> Named @"on-conversation-updated" onConversationUpdated :<|> Named @"on-user-deleted-conversations" onUserDeleted - -onClientRemoved :: - ( Member BackendNotificationQueueAccess r, - Member ConversationStore r, - Member ExternalAccess r, - Member (Error FederationError) r, - Member NotificationSubsystem r, - Member (Input Env) r, - Member (Input (Local ())) r, - Member Now r, - Member ProposalStore r, - Member Random r, - Member TinyLog r, - Member (Input ConversationSubsystemConfig) r - ) => - Domain -> - ClientRemovedRequest -> - Sem r EmptyResponse -onClientRemoved domain req = do - let qusr = Qualified req.user domain - whenM isMLSEnabled $ do - for_ req.convs $ \convId -> do - mConv <- E.getConversation convId - for mConv $ \conv -> do - lconv <- qualifyLocal conv - removeClient lconv qusr (req.client) - pure EmptyResponse - -onConversationCreated :: - ( Member BrigAPIAccess r, - Member NotificationSubsystem r, - Member ExternalAccess r, - Member (Input (Local ())) r, - Member ConversationStore r, - Member P.TinyLog r - ) => - Domain -> - ConversationCreated ConvId -> - Sem r EmptyResponse -onConversationCreated domain rc = do - let qrc = fmap (toRemoteUnsafe domain) rc - loc <- qualifyLocal () - let (localUserIds, _) = partitionQualified loc (map omQualifiedId (toList (nonCreatorMembers rc))) - - addedUserIds <- - addLocalUsersToRemoteConv - (cnvId qrc) - (tUntagged (ccRemoteOrigUserId qrc)) - localUserIds - - let connectedMembers = - Set.filter - ( foldQualified - loc - (flip Set.member addedUserIds . tUnqualified) - (const True) - . omQualifiedId - ) - (nonCreatorMembers rc) - -- Make sure to notify only about local users connected to the adder - let qrcConnected = qrc {nonCreatorMembers = connectedMembers} - - for_ (fromConversationCreated loc qrcConnected) $ \(mem, c) -> do - let event = - Event - (tUntagged (cnvId qrcConnected)) - Nothing - (EventFromUser (tUntagged (ccRemoteOrigUserId qrcConnected))) - qrcConnected.time - Nothing - (EdConversation c) - pushConversationEvent Nothing () event (qualifyAs loc [qUnqualified . Public.memId $ mem]) [] - pure EmptyResponse - -getConversationsV1 :: - ( Member ConversationStore r, - Member (Input (Local ())) r - ) => - Domain -> - GetConversationsRequest -> - Sem r GetConversationsResponse -getConversationsV1 domain req = - getConversationsResponseFromV2 <$> getConversations domain req - -getConversations :: - ( Member ConversationStore r, - Member (Input (Local ())) r - ) => - Domain -> - GetConversationsRequest -> - Sem r GetConversationsResponseV2 -getConversations domain (GetConversationsRequest uid cids) = do - let ruid = toRemoteUnsafe domain uid - loc <- qualifyLocal () - GetConversationsResponseV2 - . mapMaybe (Mapping.conversationToRemote (tDomain loc) ruid) - <$> E.getConversations cids - --- | Update the local database with information on conversation members joining --- or leaving. Finally, push out notifications to local users. -onConversationUpdated :: - ( Member BrigAPIAccess r, - Member NotificationSubsystem r, - Member ExternalAccess r, - Member (Input (Local ())) r, - Member ConversationStore r, - Member P.TinyLog r - ) => - Domain -> - ConversationUpdate -> - Sem r EmptyResponse -onConversationUpdated requestingDomain cu = do - let rcu = toRemoteUnsafe requestingDomain cu - void $ updateLocalStateOfRemoteConv rcu Nothing - pure EmptyResponse - -onConversationUpdatedV0 :: - ( Member BrigAPIAccess r, - Member NotificationSubsystem r, - Member ExternalAccess r, - Member (Input (Local ())) r, - Member ConversationStore r, - Member P.TinyLog r - ) => - Domain -> - ConversationUpdateV0 -> - Sem r EmptyResponse -onConversationUpdatedV0 domain cu = - onConversationUpdated domain (conversationUpdateFromV0 cu) - --- as of now this will not generate the necessary events on the leaver's domain -leaveConversation :: - ( Member BackendNotificationQueueAccess r, - Member ConversationStore r, - Member (Error InternalError) r, - Member ExternalAccess r, - Member (FederationAPIAccess FederatorClient) r, - Member ConversationSubsystem r, - Member NotificationSubsystem r, - Member (Input Env) r, - Member (Input (Local ())) r, - Member Now r, - Member ProposalStore r, - Member Random r, - Member TinyLog r, - Member FederationSubsystem r, - Member TeamSubsystem r, - Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r, - Member (Input ConversationSubsystemConfig) r - ) => - Domain -> - LeaveConversationRequest -> - Sem r LeaveConversationResponse -leaveConversation requestingDomain lc = do - let leaver = Qualified lc.leaver requestingDomain - lcnv <- qualifyLocal lc.convId - - res <- - runError - . mapToRuntimeError @'ConvNotFound RemoveFromConversationErrorNotFound - . mapToRuntimeError @('ActionDenied 'LeaveConversation) RemoveFromConversationErrorRemovalNotAllowed - . mapToRuntimeError @'InvalidOperation RemoveFromConversationErrorRemovalNotAllowed - . mapError @NoChanges (const RemoveFromConversationErrorUnchanged) - $ do - conv <- maskConvAccessDenied $ getConversationAsMember leaver lcnv - outcome <- - runError @FederationError $ - lcuUpdate - <$> updateLocalConversation - @'ConversationLeaveTag - lcnv - leaver - Nothing - () - case outcome of - Left e -> do - logFederationError lcnv e - throw . internalErr $ e - Right _ -> pure conv - - case res of - Left e -> pure $ LeaveConversationResponse (Left e) - Right conv -> do - let remotes = filter ((== qDomain leaver) . tDomain) ((.id_) <$> conv.remoteMembers) - let botsAndMembers = BotsAndMembers mempty (Set.fromList remotes) mempty - do - outcome <- - runError @FederationError $ - sendConversationActionNotifications - SConversationLeaveTag - leaver - False - Nothing - (qualifyAs lcnv conv) - botsAndMembers - () - def - case outcome of - Left e -> do - logFederationError lcnv e - throw . internalErr $ e - Right _ -> pure () - - pure $ LeaveConversationResponse (Right ()) - where - internalErr = InternalErrorWithDescription . LT.pack . displayExceptionNoBacktrace - --- FUTUREWORK: report errors to the originating backend --- FUTUREWORK: error handling for missing / mismatched clients --- FUTUREWORK: support bots -onMessageSent :: - ( Member NotificationSubsystem r, - Member ExternalAccess r, - Member ConversationStore r, - Member (Input (Local ())) r, - Member P.TinyLog r - ) => - Domain -> - RemoteMessage ConvId -> - Sem r EmptyResponse -onMessageSent domain rmUnqualified = do - let rm = fmap (toRemoteUnsafe domain) rmUnqualified - convId = tUntagged rm.conversation - msgMetadata = - MessageMetadata - { mmNativePush = rm.push, - mmTransient = rm.transient, - mmNativePriority = rm.priority, - mmData = _data rm - } - recipientMap = userClientMap rm.recipients - msgs = toMapOf (itraversed <.> itraversed) recipientMap - (members, allMembers) <- - first Set.fromList - <$> E.selectRemoteMembers (Map.keys recipientMap) rm.conversation - unless allMembers $ - P.warn $ - Log.field "conversation" (toByteString' (qUnqualified convId)) - Log.~~ Log.field "domain" (toByteString' (qDomain convId)) - Log.~~ Log.msg - ( "Attempt to send remote message to local\ - \ users not in the conversation" :: - ByteString - ) - loc <- qualifyLocal () - void $ - sendLocalMessages - loc - rm.time - rm.sender - rm.senderClient - Nothing - (Just convId) - mempty - msgMetadata - (Map.filterWithKey (\(uid, _) _ -> Set.member uid members) msgs) - pure EmptyResponse - -sendMessage :: - ( Member BrigAPIAccess r, - Member UserClientIndexStore r, - Member ConversationStore r, - Member (Error InvalidInput) 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 TeamSubsystem r, - Member P.TinyLog r - ) => - Domain -> - ProteusMessageSendRequest -> - Sem r MessageSendResponse -sendMessage originDomain msr = do - let sender = Qualified msr.sender originDomain - msg <- either throwErr pure (fromProto (fromBase64ByteString msr.rawMessage)) - lcnv <- qualifyLocal msr.convId - MessageSendResponse <$> postQualifiedOtrMessage User sender Nothing lcnv msg - where - throwErr = throw . InvalidPayload . LT.pack - -onUserDeleted :: - ( Member BackendNotificationQueueAccess r, - Member ConversationStore r, - Member FireAndForget r, - Member (Error FederationError) r, - Member ExternalAccess r, - Member ConversationSubsystem r, - Member NotificationSubsystem r, - Member (Input (Local ())) r, - Member Now r, - Member ProposalStore r, - Member Random r, - Member TinyLog r, - Member (Input ConversationSubsystemConfig) r - ) => - Domain -> - UserDeletedConversationsNotification -> - Sem r EmptyResponse -onUserDeleted origDomain udcn = do - let deletedUser = toRemoteUnsafe origDomain udcn.user - untaggedDeletedUser = tUntagged deletedUser - convIds = conversations udcn - - E.spawnMany $ - fromRange convIds <&> \c -> do - lc <- qualifyLocal c - mconv <- E.getConversation c - E.deleteMembers c (UserList [] [deletedUser]) - for_ mconv $ \conv -> do - when (isRemoteMember deletedUser (conv.remoteMembers)) $ - case Data.convType conv of - -- No need for a notification on One2One conv as the user is being - -- deleted and that notification should suffice. - Public.One2OneConv -> pure () - -- No need for a notification on Connect Conv as there should be no - -- other user in the conv. - Public.ConnectConv -> pure () - -- The self conv cannot be on a remote backend. - Public.SelfConv -> pure () - Public.RegularConv -> do - let botsAndMembers = convBotsAndMembers conv - removeUser (qualifyAs lc conv) RemoveUserIncludeMain (tUntagged deletedUser) - outcome <- - runError @FederationError $ - sendConversationActionNotifications - (sing @'ConversationLeaveTag) - untaggedDeletedUser - False - Nothing - (qualifyAs lc conv) - botsAndMembers - () - def - case outcome of - Left e -> logFederationError lc e - Right _ -> pure () - pure EmptyResponse - -updateConversation :: - forall r. - ( Member BackendNotificationQueueAccess r, - Member BrigAPIAccess r, - Member CodeStore r, - Member FireAndForget r, - Member (Error FederationError) r, - Member (Error InvalidInput) r, - Member ExternalAccess r, - Member (FederationAPIAccess FederatorClient) r, - Member (Error InternalError) r, - Member ConversationSubsystem r, - Member NotificationSubsystem r, - Member (Input Env) r, - Member (Input Opts) r, - Member Now r, - Member LegalHoldStore r, - Member ProposalStore r, - Member TeamSubsystem r, - Member TinyLog r, - Member Resource r, - Member ConversationStore r, - Member Random r, - Member FederationSubsystem r, - Member TeamFeatureStore r, - Member (Input (Local ())) r, - Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r, - Member TeamStore r, - Member (Input ConversationSubsystemConfig) r, - Member FeaturesConfigSubsystem r - ) => - Domain -> - ConversationUpdateRequest -> - Sem r ConversationUpdateResponse -updateConversation origDomain updateRequest = do - loc <- qualifyLocal () - let rusr = toRemoteUnsafe origDomain updateRequest.user - lcnv = qualifyAs loc updateRequest.convId - - mkResponse $ case updateRequest.action of - SomeConversationAction tag action -> case tag of - SConversationJoinTag -> - mapToGalleyError @(HasConversationActionGalleyErrors 'ConversationJoinTag) - . fmap lcuUpdate - $ updateLocalConversation @'ConversationJoinTag lcnv (tUntagged rusr) Nothing action - SConversationLeaveTag -> - mapToGalleyError - @(HasConversationActionGalleyErrors 'ConversationLeaveTag) - . fmap lcuUpdate - $ updateLocalConversation @'ConversationLeaveTag lcnv (tUntagged rusr) Nothing action - SConversationRemoveMembersTag -> - mapToGalleyError - @(HasConversationActionGalleyErrors 'ConversationRemoveMembersTag) - . fmap lcuUpdate - $ updateLocalConversation @'ConversationRemoveMembersTag lcnv (tUntagged rusr) Nothing action - SConversationMemberUpdateTag -> - mapToGalleyError - @(HasConversationActionGalleyErrors 'ConversationMemberUpdateTag) - . fmap lcuUpdate - $ updateLocalConversation @'ConversationMemberUpdateTag lcnv (tUntagged rusr) Nothing action - SConversationDeleteTag -> - mapToGalleyError - @(HasConversationActionGalleyErrors 'ConversationDeleteTag) - . fmap lcuUpdate - $ updateLocalConversation @'ConversationDeleteTag lcnv (tUntagged rusr) Nothing action - SConversationRenameTag -> - mapToGalleyError - @(HasConversationActionGalleyErrors 'ConversationRenameTag) - . fmap lcuUpdate - $ updateLocalConversation @'ConversationRenameTag lcnv (tUntagged rusr) Nothing action - SConversationMessageTimerUpdateTag -> - mapToGalleyError - @(HasConversationActionGalleyErrors 'ConversationMessageTimerUpdateTag) - . fmap lcuUpdate - $ updateLocalConversation @'ConversationMessageTimerUpdateTag lcnv (tUntagged rusr) Nothing action - SConversationReceiptModeUpdateTag -> - mapToGalleyError @(HasConversationActionGalleyErrors 'ConversationReceiptModeUpdateTag) - . fmap lcuUpdate - $ updateLocalConversation @'ConversationReceiptModeUpdateTag lcnv (tUntagged rusr) Nothing action - SConversationAccessDataTag -> - mapToGalleyError - @(HasConversationActionGalleyErrors 'ConversationAccessDataTag) - . fmap lcuUpdate - $ updateLocalConversation @'ConversationAccessDataTag lcnv (tUntagged rusr) Nothing action - SConversationUpdateProtocolTag -> - mapToGalleyError - @(HasConversationActionGalleyErrors 'ConversationUpdateProtocolTag) - . fmap lcuUpdate - $ updateLocalConversation @'ConversationUpdateProtocolTag lcnv (tUntagged rusr) Nothing action - SConversationUpdateAddPermissionTag -> - mapToGalleyError - @(HasConversationActionGalleyErrors 'ConversationUpdateAddPermissionTag) - . fmap lcuUpdate - $ updateLocalConversation @'ConversationUpdateAddPermissionTag lcnv (tUntagged rusr) Nothing action - SConversationResetTag -> - mapToGalleyError - @(HasConversationActionGalleyErrors 'ConversationResetTag) - . fmap lcuUpdate - $ updateLocalConversation @'ConversationResetTag lcnv (tUntagged rusr) Nothing action - SConversationHistoryUpdateTag -> - mapToGalleyError - @(HasConversationActionGalleyErrors 'ConversationHistoryUpdateTag) - . fmap lcuUpdate - $ updateLocalConversation @'ConversationHistoryUpdateTag lcnv (tUntagged rusr) Nothing action - where - mkResponse = - fmap (either ConversationUpdateResponseError Imports.id) - . runError @GalleyError - . fmap (fromRight ConversationUpdateResponseNoChanges) - . runError @NoChanges - . fmap (either ConversationUpdateResponseNonFederatingBackends Imports.id) - . runError @NonFederatingBackends - . fmap (either ConversationUpdateResponseUnreachableBackends Imports.id) - . runError @UnreachableBackends - . fmap ConversationUpdateResponseUpdate - -handleMLSMessageErrors :: - ( r1 - ~ Append - MLSBundleStaticErrors - ( Error MLSOutOfSyncError - ': Error GroupInfoDiagnostics - ': Error UnreachableBackends - ': Error NonFederatingBackends - ': Error MLSProposalFailure - ': Error GalleyError - ': Error MLSProtocolError - ': r - ) - ) => - Sem r1 MLSMessageResponse -> - Sem r MLSMessageResponse -handleMLSMessageErrors = - fmap (either (MLSMessageResponseProtocolError . unTagged) Imports.id) - . runError @MLSProtocolError - . fmap (either MLSMessageResponseError Imports.id) - . runError - . fmap (either (MLSMessageResponseProposalFailure . pfInner) Imports.id) - . runError - . fmap (either MLSMessageResponseNonFederatingBackends Imports.id) - . runError - . fmap (either (MLSMessageResponseUnreachableBackends . Set.fromList . (.backends)) Imports.id) - . runError @UnreachableBackends - . fmap (either MLSMessageResponseGroupInfoDiagnostics Imports.id) - . runError @GroupInfoDiagnostics - . fmap (either MLSMessageResponseOutOfSyncError Imports.id) - . runError @MLSOutOfSyncError - . mapToGalleyError @MLSBundleStaticErrors - -sendMLSCommitBundle :: - ( Member BackendNotificationQueueAccess r, - Member BrigAPIAccess r, - Member ConversationStore r, - Member ExternalAccess r, - Member (Error FederationError) r, - Member (Error InternalError) r, - Member (FederationAPIAccess FederatorClient) r, - Member ConversationSubsystem r, - Member NotificationSubsystem r, - Member (Input (Local ())) r, - Member (Input Env) r, - Member (Input Opts) r, - Member Now r, - Member LegalHoldStore r, - Member Resource r, - Member TeamStore r, - Member FederationSubsystem r, - Member TeamSubsystem r, - Member P.TinyLog r, - Member Random r, - Member ProposalStore r, - Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r, - Member FeaturesConfigSubsystem r, - Member (Input ConversationSubsystemConfig) r - ) => - Domain -> - MLSMessageSendRequest -> - Sem r MLSMessageResponse -sendMLSCommitBundle remoteDomain msr = handleMLSMessageErrors $ do - assertMLSEnabled - loc <- qualifyLocal () - let sender = toRemoteUnsafe remoteDomain msr.sender - bundle <- - either (throw . mlsProtocolError) pure $ - decodeMLS' (fromBase64ByteString msr.rawMessage) - - ibundle <- noteS @'MLSUnsupportedMessage $ mkIncomingBundle bundle - (ctype, qConvOrSub) <- getConvFromGroupId ibundle.groupId - when (qUnqualified qConvOrSub /= msr.convOrSubId) $ throwS @'MLSGroupConversationMismatch - - -- this cannot throw the error since we always pass the sender which is qualified to be remote - runInputConst (fromMaybe def msr.enableOutOfSyncCheck) $ - MLSMessageResponseUpdates - . fmap lcuUpdate - <$> mapToRuntimeError @MLSLegalholdIncompatible - (InternalErrorWithDescription "expected group conversation while handling policy conflicts") - ( postMLSCommitBundle - loc - -- Type application to prevent future changes from introducing errors. - -- It is only safe to assume that we can discard the error when the sender - -- is actually remote. - -- Since `tUntagged` works on local and remote, a future changed may - -- go unchecked without this. - (tUntagged @QRemote sender) - msr.senderClient - ctype - qConvOrSub - Nothing - ibundle - ) - -sendMLSMessage :: - ( Member BackendNotificationQueueAccess r, - Member BrigAPIAccess r, - Member ConversationStore r, - Member ExternalAccess r, - Member (Error FederationError) r, - Member (Error InternalError) 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 P.TinyLog r, - Member ProposalStore r, - Member TeamCollaboratorsSubsystem r, - Member TeamStore r - ) => - Domain -> - MLSMessageSendRequest -> - Sem r MLSMessageResponse -sendMLSMessage remoteDomain msr = handleMLSMessageErrors $ do - assertMLSEnabled - loc <- qualifyLocal () - let sender = toRemoteUnsafe remoteDomain msr.sender - raw <- either (throw . mlsProtocolError) pure $ decodeMLS' (fromBase64ByteString msr.rawMessage) - msg <- noteS @'MLSUnsupportedMessage $ mkIncomingMessage raw - (ctype, qConvOrSub) <- getConvFromGroupId msg.groupId - when (qUnqualified qConvOrSub /= msr.convOrSubId) $ throwS @'MLSGroupConversationMismatch - runInputConst (fromMaybe def msr.enableOutOfSyncCheck) $ - MLSMessageResponseUpdates . map lcuUpdate - <$> postMLSMessage - loc - (tUntagged sender) - msr.senderClient - ctype - qConvOrSub - Nothing - msg - -getSubConversationForRemoteUser :: - ( Member ConversationStore r, - Member (Input (Local ())) r, - Member TeamSubsystem r - ) => - Domain -> - GetSubConversationsRequest -> - Sem r GetSubConversationsResponse -getSubConversationForRemoteUser domain GetSubConversationsRequest {..} = - fmap (either GetSubConversationsResponseError GetSubConversationsResponseSuccess) - . runError @GalleyError - . mapToGalleyError @MLSGetSubConvStaticErrors - $ do - let qusr = Qualified gsreqUser domain - lconv <- qualifyLocal gsreqConv - getLocalSubConversation qusr lconv gsreqSubConv - -leaveSubConversation :: - ( HasLeaveSubConversationEffects r, - Member (Error FederationError) r, - Member (Input (Local ())) r, - Member Resource r, - Member TeamSubsystem r, - Member E.MLSCommitLockStore r, - Member (Input ConversationSubsystemConfig) r - ) => - Domain -> - LeaveSubConversationRequest -> - Sem r LeaveSubConversationResponse -leaveSubConversation domain lscr = do - let rusr = toRemoteUnsafe domain (lscrUser lscr) - cid = mkClientIdentity (tUntagged rusr) (lscrClient lscr) - lcnv <- qualifyLocal (lscrConv lscr) - fmap (either (LeaveSubConversationResponseProtocolError . unTagged) Imports.id) - . runError @MLSProtocolError - . fmap (either LeaveSubConversationResponseError Imports.id) - . runError @GalleyError - . mapToGalleyError @LeaveSubConversationStaticErrors - $ leaveLocalSubConversation cid lcnv (lscrSubConv lscr) - $> LeaveSubConversationResponseOk - -deleteSubConversationForRemoteUser :: - ( Member ConversationStore r, - Member (Input (Local ())) r, - Member Resource r, - Member TeamSubsystem r, - Member E.MLSCommitLockStore r - ) => - Domain -> - DeleteSubConversationFedRequest -> - Sem r DeleteSubConversationResponse -deleteSubConversationForRemoteUser domain DeleteSubConversationFedRequest {..} = - fmap - ( either - DeleteSubConversationResponseError - (\() -> DeleteSubConversationResponseSuccess) - ) - . runError @GalleyError - . mapToGalleyError @MLSDeleteSubConvStaticErrors - $ do - let qusr = Qualified dscreqUser domain - dsc = MLSReset dscreqGroupId dscreqEpoch - lconv <- qualifyLocal dscreqConv - resetLocalSubConversation qusr lconv dscreqSubConv dsc - -getOne2OneConversationV1 :: - ( Member (Input (Local ())) r, - Member BrigAPIAccess r, - Member (Error InvalidInput) r - ) => - Domain -> - GetOne2OneConversationRequest -> - Sem r GetOne2OneConversationResponse -getOne2OneConversationV1 domain (GetOne2OneConversationRequest self other) = - fmap (Imports.fromRight GetOne2OneConversationNotConnected) - . runError @(Tagged 'NotConnected ()) - $ do - lother <- qualifyLocal other - let rself = toRemoteUnsafe domain self - ensureConnectedToRemotes lother [rself] - foldQualified - lother - (const . throw $ FederationFunctionNotSupported "Getting 1:1 conversations is not supported over federation API < V2.") - (const (pure GetOne2OneConversationBackendMismatch)) - (one2OneConvId BaseProtocolMLSTag (tUntagged lother) (tUntagged rself)) - -getOne2OneConversation :: - ( Member ConversationStore r, - Member (Input (Local ())) r, - Member (Error InternalError) r, - Member BrigAPIAccess r, - Member (Input Env) r - ) => - Domain -> - GetOne2OneConversationRequest -> - Sem r GetOne2OneConversationResponseV2 -getOne2OneConversation domain (GetOne2OneConversationRequest self other) = - fmap (Imports.fromRight GetOne2OneConversationV2MLSNotEnabled) - . runError @(Tagged 'MLSNotEnabled ()) - . fmap (Imports.fromRight GetOne2OneConversationV2NotConnected) - . runError @(Tagged 'NotConnected ()) - $ do - lother <- qualifyLocal other - let rself = toRemoteUnsafe domain self - let getLocal lconv = do - mconv <- E.getConversation (tUnqualified lconv) - mlsPublicKeys <- mlsKeysToPublic <$$> getMLSPrivateKeys - conv <- case mconv of - Nothing -> pure (localMLSOne2OneConversationAsRemote lconv) - Just conv -> - note - (InternalErrorWithDescription "Unexpected member list in 1-1 conversation") - (conversationToRemote (tDomain lother) rself conv) - pure . GetOne2OneConversationV2Ok $ RemoteMLSOne2OneConversation conv mlsPublicKeys - - ensureConnectedToRemotes lother [rself] - - foldQualified - lother - getLocal - (const (pure GetOne2OneConversationV2BackendMismatch)) - (one2OneConvId BaseProtocolMLSTag (tUntagged lother) (tUntagged rself)) - --------------------------------------------------------------------------------- --- Error handling machinery - -class ToGalleyRuntimeError (effs :: EffectRow) r where - mapToGalleyError :: - (Member (Error GalleyError) r) => - Sem (Append effs r) a -> - Sem r a - -instance ToGalleyRuntimeError '[] r where - mapToGalleyError = Imports.id - -instance - forall (err :: GalleyError) effs r. - ( ToGalleyRuntimeError effs r, - SingI err, - Member (Error GalleyError) (Append effs r) - ) => - ToGalleyRuntimeError (ErrorS err ': effs) r - where - mapToGalleyError act = - mapToGalleyError @effs @r $ - runError act >>= \case - Left _ -> throw (demote @err) - Right res -> pure res - -onMLSMessageSent :: - ( Member ExternalAccess r, - Member NotificationSubsystem r, - Member (Input (Local ())) r, - Member (Input Env) r, - Member ConversationStore r, - Member P.TinyLog r - ) => - Domain -> - RemoteMLSMessage -> - Sem r EmptyResponse -onMLSMessageSent domain rmm = - (EmptyResponse <$) - . (logError =<<) - . runError @(Tagged 'MLSNotEnabled ()) - $ do - assertMLSEnabled - loc <- qualifyLocal () - let rcnv = toRemoteUnsafe domain rmm.conversation - let users = Map.keys rmm.recipients - (members, allMembers) <- - first Set.fromList - <$> E.selectRemoteMembers (toList users) rcnv - unless allMembers $ - P.warn $ - Log.field "conversation" (toByteString' (tUnqualified rcnv)) - Log.~~ Log.field "domain" (toByteString' (tDomain rcnv)) - Log.~~ Log.msg - ( "Attempt to send remote message to local\ - \ users not in the conversation" :: - ByteString - ) - let recipients = - filter (\r -> Set.member (recipientUserId r) members) - . map (\(u, clts) -> Recipient u (RecipientClientsSome clts)) - . Map.assocs - $ rmm.recipients - -- FUTUREWORK: support local bots - let e = - Event (tUntagged rcnv) rmm.subConversation (EventFromUser rmm.sender) rmm.time Nothing $ - EdMLSMessage (fromBase64ByteString rmm.message) - - runMessagePush loc (Just (tUntagged rcnv)) $ - newMessagePush mempty Nothing rmm.metadata recipients e - where - logError :: (Member P.TinyLog r) => Either (Tagged 'MLSNotEnabled ()) () -> Sem r () - logError (Left _) = - P.warn $ - Log.field "conversation" (toByteString' rmm.conversation) - Log.~~ Log.field "domain" (toByteString' domain) - Log.~~ Log.msg - ("Cannot process remote MLS message because MLS is disabled on this backend" :: ByteString) - logError _ = pure () - -mlsSendWelcome :: - ( Member (Error InternalError) r, - Member NotificationSubsystem r, - Member ExternalAccess r, - Member P.TinyLog r, - Member (Input Env) r, - Member (Input (Local ())) r, - Member Now r - ) => - Domain -> - MLSWelcomeRequest -> - Sem r MLSWelcomeResponse -mlsSendWelcome origDomain req = do - fmap (either (const MLSWelcomeMLSNotEnabled) (const MLSWelcomeSent)) - . runError @(Tagged 'MLSNotEnabled ()) - $ do - assertMLSEnabled - loc <- qualifyLocal () - now <- Now.get - welcome <- - either (throw . InternalErrorWithDescription . LT.fromStrict) pure $ - decodeMLS' (fromBase64ByteString req.welcomeMessage) - sendLocalWelcomes req.qualifiedConvId (Qualified req.originatingUser origDomain) Nothing now welcome (qualifyAs loc req.recipients) - -queryGroupInfo :: - ( Member ConversationStore r, - Member (Input (Local ())) r, - Member (Input Env) r - ) => - Domain -> - GetGroupInfoRequest -> - Sem r GetGroupInfoResponse -queryGroupInfo origDomain req = - fmap (either GetGroupInfoResponseError GetGroupInfoResponseState) - . runError @GalleyError - . mapToGalleyError @MLSGroupInfoStaticErrors - $ do - assertMLSEnabled - let sender = toRemoteUnsafe origDomain . (.sender) $ req - state <- case req.conv of - Conv convId -> do - lconvId <- qualifyLocal convId - getGroupInfoFromLocalConv (tUntagged sender) lconvId - SubConv convId subConvId -> do - lconvId <- qualifyLocal convId - getSubConversationGroupInfoFromLocalConv (tUntagged sender) subConvId lconvId - pure - . Base64ByteString - . unGroupInfoData - $ state - -updateTypingIndicator :: - ( Member NotificationSubsystem r, - Member (FederationAPIAccess FederatorClient) r, - Member ConversationStore r, - Member Now r, - Member (Input (Local ())) r, - Member TeamSubsystem r - ) => - Domain -> - TypingDataUpdateRequest -> - Sem r TypingDataUpdateResponse -updateTypingIndicator origDomain TypingDataUpdateRequest {..} = do - let qusr = Qualified userId origDomain - lcnv <- qualifyLocal convId - - ret <- runError - . mapToRuntimeError @'ConvNotFound ConvNotFound - $ do - conv <- maskConvAccessDenied $ getConversationAsMember qusr lcnv - notifyTypingIndicator conv qusr Nothing typingStatus - - pure (either TypingDataUpdateError TypingDataUpdateSuccess ret) - -onTypingIndicatorUpdated :: - (Member NotificationSubsystem r) => - Domain -> - TypingDataUpdated -> - Sem r EmptyResponse -onTypingIndicatorUpdated origDomain TypingDataUpdated {..} = do - let qcnv = Qualified convId origDomain - pushTypingIndicatorEvents origUserId time usersInConv Nothing qcnv typingStatus - pure EmptyResponse - --------------------------------------------------------------------------------- --- Utilities --------------------------------------------------------------------------------- - --- | Log a federation error that is impossible in processing a remote request --- for a local conversation. -logFederationError :: - (Member P.TinyLog r) => - Local ConvId -> - FederationError -> - Sem r () -logFederationError lc e = - P.warn $ - Log.field "conversation" (toByteString' (tUnqualified lc)) - Log.~~ Log.field "domain" (toByteString' (tDomain lc)) - Log.~~ Log.msg - ( "An impossible federation error occurred when deleting\ - \ a user from a local conversation: " - <> displayException e - ) diff --git a/services/galley/src/Galley/API/Federation/Handlers.hs b/services/galley/src/Galley/API/Federation/Handlers.hs new file mode 100644 index 00000000000..1fa2540994a --- /dev/null +++ b/services/galley/src/Galley/API/Federation/Handlers.hs @@ -0,0 +1,1003 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2026 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Galley.API.Federation.Handlers where + +import Control.Error hiding (note) +import Control.Lens +import Data.Bifunctor +import Data.ByteString.Conversion (toByteString') +import Data.Default +import Data.Domain (Domain) +import Data.Id +import Data.Json.Util +import Data.Map qualified as Map +import Data.Map.Lens (toMapOf) +import Data.Qualified +import Data.Range (Range (fromRange)) +import Data.Set qualified as Set +import Data.Singletons (SingI (..), demote, sing) +import Data.Tagged +import Data.Text.Lazy qualified as LT +import Galley.API.Action +import Galley.API.MLS +import Galley.API.MLS.Enabled +import Galley.API.MLS.GroupInfo +import Galley.API.MLS.GroupInfoCheck (GroupInfoCheckEnabled) +import Galley.API.MLS.Message +import Galley.API.MLS.One2One +import Galley.API.MLS.Removal +import Galley.API.MLS.SubConversation hiding (leaveSubConversation) +import Galley.API.MLS.Util +import Galley.API.MLS.Welcome +import Galley.API.Mapping +import Galley.API.Mapping qualified as Mapping +import Galley.API.Message +import Galley.Options +import Galley.Types.Conversations.One2One +import Galley.Types.Error +import Imports +import Network.Wai.Utilities.Exception +import Polysemy +import Polysemy.Error +import Polysemy.Input +import Polysemy.Internal.Kind (Append) +import Polysemy.Resource +import Polysemy.TinyLog +import Polysemy.TinyLog qualified as P +import System.Logger.Class qualified as Log +import Wire.API.Conversation hiding (Member) +import Wire.API.Conversation qualified as Public +import Wire.API.Conversation.Action +import Wire.API.Conversation.Config (ConversationSubsystemConfig) +import Wire.API.Conversation.Role +import Wire.API.Error +import Wire.API.Error.Galley +import Wire.API.Event.Conversation +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.Error +import Wire.API.MLS.Credential +import Wire.API.MLS.GroupInfo +import Wire.API.MLS.Keys +import Wire.API.MLS.Serialisation +import Wire.API.MLS.SubConversation +import Wire.API.Message +import Wire.API.Push.V2 (RecipientClients (..)) +import Wire.API.Routes.Public.Galley.MLS +import Wire.API.ServantProto +import Wire.API.User (BaseProtocolTag (..)) +import Wire.BackendNotificationQueueAccess +import Wire.BrigAPIAccess (BrigAPIAccess) +import Wire.CodeStore +import Wire.ConversationStore qualified as E +import Wire.ConversationSubsystem +import Wire.ConversationSubsystem.Util +import Wire.ExternalAccess (ExternalAccess) +import Wire.FeaturesConfigSubsystem +import Wire.FederationAPIAccess (FederationAPIAccess) +import Wire.FederationSubsystem (FederationSubsystem) +import Wire.FireAndForget qualified as E +import Wire.LegalHoldStore (LegalHoldStore) +import Wire.NotificationSubsystem +import Wire.ProposalStore (ProposalStore) +import Wire.Sem.Now (Now) +import Wire.Sem.Now qualified as Now +import Wire.Sem.Random (Random) +import Wire.StoredConversation +import Wire.StoredConversation qualified as Data +import Wire.TeamCollaboratorsSubsystem +import Wire.TeamStore +import Wire.TeamSubsystem (TeamSubsystem) +import Wire.UserClientIndexStore (UserClientIndexStore) +import Wire.UserList (UserList (UserList)) + +onClientRemoved :: + ( Member BackendNotificationQueueAccess r, + Member E.ConversationStore r, + Member ExternalAccess r, + Member (Error FederationError) r, + Member NotificationSubsystem r, + Member (Input (Maybe (MLSKeysByPurpose MLSPrivateKeys))) r, + Member (Input (Local ())) r, + Member Now r, + Member ProposalStore r, + Member Random r, + Member TinyLog r, + Member (Input ConversationSubsystemConfig) r + ) => + Domain -> + ClientRemovedRequest -> + Sem r EmptyResponse +onClientRemoved domain req = do + let qusr = Qualified req.user domain + whenM isMLSEnabled $ do + for_ req.convs $ \convId -> do + mConv <- E.getConversation convId + for mConv $ \conv -> do + lconv <- qualifyLocal conv + removeClient lconv qusr (req.client) + pure EmptyResponse + +onConversationCreated :: + ( Member BrigAPIAccess r, + Member NotificationSubsystem r, + Member ExternalAccess r, + Member (Input (Local ())) r, + Member E.ConversationStore r, + Member P.TinyLog r + ) => + Domain -> + ConversationCreated ConvId -> + Sem r EmptyResponse +onConversationCreated domain rc = do + let qrc = fmap (toRemoteUnsafe domain) rc + loc <- qualifyLocal () + let (localUserIds, _) = partitionQualified loc (map omQualifiedId (toList (nonCreatorMembers rc))) + + addedUserIds <- + addLocalUsersToRemoteConv + (cnvId qrc) + (tUntagged (ccRemoteOrigUserId qrc)) + localUserIds + + let connectedMembers = + Set.filter + ( foldQualified + loc + (flip Set.member addedUserIds . tUnqualified) + (const True) + . omQualifiedId + ) + (nonCreatorMembers rc) + -- Make sure to notify only about local users connected to the adder + let qrcConnected = qrc {nonCreatorMembers = connectedMembers} + + for_ (fromConversationCreated loc qrcConnected) $ \(mem, c) -> do + let event = + Event + (tUntagged (cnvId qrcConnected)) + Nothing + (EventFromUser (tUntagged (ccRemoteOrigUserId qrcConnected))) + qrcConnected.time + Nothing + (EdConversation c) + pushConversationEvent Nothing () event (qualifyAs loc [qUnqualified . Public.memId $ mem]) [] + pure EmptyResponse + +getConversationsV1 :: + ( Member E.ConversationStore r, + Member (Input (Local ())) r + ) => + Domain -> + GetConversationsRequest -> + Sem r GetConversationsResponse +getConversationsV1 domain req = + getConversationsResponseFromV2 <$> Galley.API.Federation.Handlers.getConversations domain req + +getConversations :: + ( Member E.ConversationStore r, + Member (Input (Local ())) r + ) => + Domain -> + GetConversationsRequest -> + Sem r GetConversationsResponseV2 +getConversations domain (GetConversationsRequest uid cids) = do + let ruid = toRemoteUnsafe domain uid + loc <- qualifyLocal () + GetConversationsResponseV2 + . mapMaybe (Mapping.conversationToRemote (tDomain loc) ruid) + <$> E.getConversations cids + +-- | Update the local database with information on conversation members joining +-- or leaving. Finally, push out notifications to local users. +onConversationUpdated :: + ( Member BrigAPIAccess r, + Member NotificationSubsystem r, + Member ExternalAccess r, + Member (Input (Local ())) r, + Member E.ConversationStore r, + Member P.TinyLog r + ) => + Domain -> + ConversationUpdate -> + Sem r EmptyResponse +onConversationUpdated requestingDomain cu = do + let rcu = toRemoteUnsafe requestingDomain cu + void $ updateLocalStateOfRemoteConv rcu Nothing + pure EmptyResponse + +onConversationUpdatedV0 :: + ( Member BrigAPIAccess r, + Member NotificationSubsystem r, + Member ExternalAccess r, + Member (Input (Local ())) r, + Member E.ConversationStore r, + Member P.TinyLog r + ) => + Domain -> + ConversationUpdateV0 -> + Sem r EmptyResponse +onConversationUpdatedV0 domain cu = + onConversationUpdated domain (conversationUpdateFromV0 cu) + +-- as of now this will not generate the necessary events on the leaver's domain +leaveConversation :: + ( Member BackendNotificationQueueAccess r, + Member E.ConversationStore r, + Member (Error InternalError) r, + Member ExternalAccess r, + Member ConversationSubsystem r, + Member NotificationSubsystem r, + Member (Input (Local ())) r, + Member Now r, + Member ProposalStore r, + Member Random r, + Member TinyLog r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r + ) => + Domain -> + LeaveConversationRequest -> + Sem r LeaveConversationResponse +leaveConversation requestingDomain lc = do + let leaver = Qualified lc.leaver requestingDomain + lcnv <- qualifyLocal lc.convId + + res <- + runError + . mapToRuntimeError @'ConvNotFound RemoveFromConversationErrorNotFound + . mapToRuntimeError @('ActionDenied 'LeaveConversation) RemoveFromConversationErrorRemovalNotAllowed + . mapToRuntimeError @'InvalidOperation RemoveFromConversationErrorRemovalNotAllowed + . mapError @NoChanges (const RemoveFromConversationErrorUnchanged) + $ do + conv <- maskConvAccessDenied $ getConversationAsMember leaver lcnv + outcome <- + runError @FederationError $ + lcuUpdate + <$> updateLocalConversationLeave + lcnv + leaver + Nothing + case outcome of + Left e -> do + logFederationError lcnv e + throw . internalErr $ e + Right _ -> pure conv + + case res of + Left e -> pure $ LeaveConversationResponse (Left e) + Right conv -> do + let remotes = filter ((== qDomain leaver) . tDomain) ((.id_) <$> conv.remoteMembers) + let botsAndMembers = BotsAndMembers mempty (Set.fromList remotes) mempty + do + outcome <- + runError @FederationError $ + sendConversationActionNotifications + SConversationLeaveTag + leaver + False + Nothing + (qualifyAs lcnv conv) + botsAndMembers + () + def + case outcome of + Left e -> do + logFederationError lcnv e + throw . internalErr $ e + Right _ -> pure () + + pure $ LeaveConversationResponse (Right ()) + where + internalErr = InternalErrorWithDescription . LT.pack . displayExceptionNoBacktrace + +-- FUTUREWORK: report errors to the originating backend +-- FUTUREWORK: error handling for missing / mismatched clients +-- FUTUREWORK: support bots +onMessageSent :: + ( Member NotificationSubsystem r, + Member ExternalAccess r, + Member E.ConversationStore r, + Member (Input (Local ())) r, + Member P.TinyLog r + ) => + Domain -> + RemoteMessage ConvId -> + Sem r EmptyResponse +onMessageSent domain rmUnqualified = do + let rm = fmap (toRemoteUnsafe domain) rmUnqualified + convId = tUntagged rm.conversation + msgMetadata = + MessageMetadata + { mmNativePush = rm.push, + mmTransient = rm.transient, + mmNativePriority = rm.priority, + mmData = _data rm + } + recipientMap = userClientMap rm.recipients + msgs = toMapOf (itraversed <.> itraversed) recipientMap + (members, allMembers) <- + first Set.fromList + <$> E.selectRemoteMembers (Map.keys recipientMap) rm.conversation + unless allMembers $ + P.warn $ + Log.field "conversation" (toByteString' (qUnqualified convId)) + Log.~~ Log.field "domain" (toByteString' (qDomain convId)) + Log.~~ Log.msg + ( "Attempt to send remote message to local\ + \ users not in the conversation" :: + ByteString + ) + loc <- qualifyLocal () + void $ + sendLocalMessages + loc + rm.time + rm.sender + rm.senderClient + Nothing + (Just convId) + mempty + msgMetadata + (Map.filterWithKey (\(uid, _) _ -> Set.member uid members) msgs) + pure EmptyResponse + +sendMessage :: + ( Member BrigAPIAccess r, + Member UserClientIndexStore r, + Member E.ConversationStore r, + Member (Error InvalidInput) 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 TeamSubsystem r, + Member P.TinyLog r + ) => + Domain -> + ProteusMessageSendRequest -> + Sem r MessageSendResponse +sendMessage originDomain msr = do + let sender = Qualified msr.sender originDomain + msg <- either throwErr pure (fromProto (fromBase64ByteString msr.rawMessage)) + lcnv <- qualifyLocal msr.convId + MessageSendResponse <$> postQualifiedOtrMessage User sender Nothing lcnv msg + where + throwErr = throw . InvalidPayload . LT.pack + +onUserDeleted :: + ( Member BackendNotificationQueueAccess r, + Member E.ConversationStore r, + Member E.FireAndForget r, + Member (Error FederationError) r, + Member ExternalAccess r, + Member ConversationSubsystem r, + Member NotificationSubsystem r, + Member (Input (Local ())) r, + Member Now r, + Member ProposalStore r, + Member Random r, + Member TinyLog r, + Member (Input ConversationSubsystemConfig) r + ) => + Domain -> + UserDeletedConversationsNotification -> + Sem r EmptyResponse +onUserDeleted origDomain udcn = do + let deletedUser = toRemoteUnsafe origDomain udcn.user + untaggedDeletedUser = tUntagged deletedUser + convIds = conversations udcn + + E.spawnMany $ + fromRange convIds <&> \c -> do + lc <- qualifyLocal c + mconv <- E.getConversation c + E.deleteMembers c (UserList [] [deletedUser]) + for_ mconv $ \conv -> do + when (isRemoteMember deletedUser (conv.remoteMembers)) $ + case Data.convType conv of + -- No need for a notification on One2One conv as the user is being + -- deleted and that notification should suffice. + Public.One2OneConv -> pure () + -- No need for a notification on Connect Conv as there should be no + -- other user in the conv. + Public.ConnectConv -> pure () + -- The self conv cannot be on a remote backend. + Public.SelfConv -> pure () + Public.RegularConv -> do + let botsAndMembers = convBotsAndMembers conv + removeUser (qualifyAs lc conv) RemoveUserIncludeMain (tUntagged deletedUser) + outcome <- + runError @FederationError $ + sendConversationActionNotifications + (sing @'ConversationLeaveTag) + untaggedDeletedUser + False + Nothing + (qualifyAs lc conv) + botsAndMembers + () + def + case outcome of + Left e -> logFederationError lc e + Right _ -> pure () + pure EmptyResponse + +updateConversation :: + forall r. + ( Member BackendNotificationQueueAccess r, + Member BrigAPIAccess r, + Member CodeStore r, + Member E.FireAndForget r, + Member (Error FederationError) r, + Member (Error InvalidInput) r, + Member ExternalAccess r, + Member (FederationAPIAccess FederatorClient) r, + Member ConversationSubsystem r, + Member NotificationSubsystem r, + Member Now r, + Member LegalHoldStore r, + Member ProposalStore r, + Member TeamSubsystem r, + Member TinyLog r, + Member Resource r, + Member E.ConversationStore r, + Member Random r, + Member FederationSubsystem r, + Member (Input (Local ())) r, + Member TeamCollaboratorsSubsystem r, + Member E.MLSCommitLockStore r, + Member TeamStore r, + Member (Input ConversationSubsystemConfig) r, + Member FeaturesConfigSubsystem r + ) => + Domain -> + ConversationUpdateRequest -> + Sem r ConversationUpdateResponse +updateConversation origDomain updateRequest = do + loc <- qualifyLocal () + let rusr = toRemoteUnsafe origDomain updateRequest.user + lcnv = qualifyAs loc updateRequest.convId + + mkResponse $ case updateRequest.action of + SomeConversationAction tag action -> case tag of + SConversationJoinTag -> + mapToGalleyError @(HasConversationActionGalleyErrors 'ConversationJoinTag) + . fmap lcuUpdate + $ updateLocalConversationJoin lcnv (tUntagged rusr) Nothing action + SConversationLeaveTag -> + mapToGalleyError + @(HasConversationActionGalleyErrors 'ConversationLeaveTag) + . fmap lcuUpdate + $ updateLocalConversationLeave lcnv (tUntagged rusr) Nothing + SConversationRemoveMembersTag -> + mapToGalleyError + @(HasConversationActionGalleyErrors 'ConversationRemoveMembersTag) + . fmap lcuUpdate + $ updateLocalConversationRemoveMembers lcnv (tUntagged rusr) Nothing action + SConversationMemberUpdateTag -> + mapToGalleyError + @(HasConversationActionGalleyErrors 'ConversationMemberUpdateTag) + . fmap lcuUpdate + $ updateLocalConversationMemberUpdate lcnv (tUntagged rusr) Nothing action + SConversationDeleteTag -> + mapToGalleyError + @(HasConversationActionGalleyErrors 'ConversationDeleteTag) + . fmap lcuUpdate + $ updateLocalConversationDelete lcnv (tUntagged rusr) Nothing + SConversationRenameTag -> + mapToGalleyError + @(HasConversationActionGalleyErrors 'ConversationRenameTag) + . fmap lcuUpdate + $ updateLocalConversationRename lcnv (tUntagged rusr) Nothing action + SConversationMessageTimerUpdateTag -> + mapToGalleyError + @(HasConversationActionGalleyErrors 'ConversationMessageTimerUpdateTag) + . fmap lcuUpdate + $ updateLocalConversationMessageTimerUpdate lcnv (tUntagged rusr) Nothing action + SConversationReceiptModeUpdateTag -> + mapToGalleyError @(HasConversationActionGalleyErrors 'ConversationReceiptModeUpdateTag) + . fmap lcuUpdate + $ updateLocalConversationReceiptModeUpdate lcnv (tUntagged rusr) Nothing action + SConversationAccessDataTag -> + mapToGalleyError + @(HasConversationActionGalleyErrors 'ConversationAccessDataTag) + . fmap lcuUpdate + $ updateLocalConversationAccessData lcnv (tUntagged rusr) Nothing action + SConversationUpdateProtocolTag -> + mapToGalleyError + @(HasConversationActionGalleyErrors 'ConversationUpdateProtocolTag) + . fmap lcuUpdate + $ updateLocalConversationUpdateProtocol lcnv (tUntagged rusr) Nothing action + SConversationUpdateAddPermissionTag -> + mapToGalleyError + @(HasConversationActionGalleyErrors 'ConversationUpdateAddPermissionTag) + . fmap lcuUpdate + $ updateLocalConversationUpdateAddPermission lcnv (tUntagged rusr) Nothing action + SConversationResetTag -> + mapToGalleyError + @(HasConversationActionGalleyErrors 'ConversationResetTag) + . fmap lcuUpdate + $ updateLocalConversationReset lcnv (tUntagged rusr) Nothing action + SConversationHistoryUpdateTag -> + mapToGalleyError + @(HasConversationActionGalleyErrors 'ConversationHistoryUpdateTag) + . fmap lcuUpdate + $ updateLocalConversationHistoryUpdate lcnv (tUntagged rusr) Nothing action + where + mkResponse = + fmap (either ConversationUpdateResponseError Imports.id) + . runError @GalleyError + . fmap (fromRight ConversationUpdateResponseNoChanges) + . runError @NoChanges + . fmap (either ConversationUpdateResponseNonFederatingBackends Imports.id) + . runError @NonFederatingBackends + . fmap (either ConversationUpdateResponseUnreachableBackends Imports.id) + . runError @UnreachableBackends + . fmap ConversationUpdateResponseUpdate + +handleMLSMessageErrors :: + ( r1 + ~ Append + MLSBundleStaticErrors + ( Error MLSOutOfSyncError + ': Error GroupInfoDiagnostics + ': Error UnreachableBackends + ': Error NonFederatingBackends + ': Error MLSProposalFailure + ': Error GalleyError + ': Error MLSProtocolError + ': r + ) + ) => + Sem r1 MLSMessageResponse -> + Sem r MLSMessageResponse +handleMLSMessageErrors = + fmap (either (MLSMessageResponseProtocolError . unTagged) Imports.id) + . runError @MLSProtocolError + . fmap (either MLSMessageResponseError Imports.id) + . runError + . fmap (either (MLSMessageResponseProposalFailure . pfInner) Imports.id) + . runError + . fmap (either MLSMessageResponseNonFederatingBackends Imports.id) + . runError + . fmap (either (MLSMessageResponseUnreachableBackends . Set.fromList . (.backends)) Imports.id) + . runError @UnreachableBackends + . fmap (either MLSMessageResponseGroupInfoDiagnostics Imports.id) + . runError @GroupInfoDiagnostics + . fmap (either MLSMessageResponseOutOfSyncError Imports.id) + . runError @MLSOutOfSyncError + . mapToGalleyError @MLSBundleStaticErrors + +sendMLSCommitBundle :: + ( Member BackendNotificationQueueAccess r, + Member BrigAPIAccess r, + Member E.ConversationStore r, + Member ExternalAccess r, + Member (Error FederationError) r, + Member (Error InternalError) r, + Member (FederationAPIAccess FederatorClient) r, + Member ConversationSubsystem r, + Member NotificationSubsystem r, + Member (Input (Local ())) r, + Member (Input (Maybe GroupInfoCheckEnabled)) r, + Member (Input (Maybe (MLSKeysByPurpose MLSPrivateKeys))) r, + Member Now r, + Member LegalHoldStore r, + Member Resource r, + Member TeamStore r, + Member FederationSubsystem r, + Member TeamSubsystem r, + Member P.TinyLog r, + Member Random r, + Member ProposalStore r, + Member TeamCollaboratorsSubsystem r, + Member E.MLSCommitLockStore r, + Member FeaturesConfigSubsystem r, + Member (Input ConversationSubsystemConfig) r + ) => + Domain -> + MLSMessageSendRequest -> + Sem r MLSMessageResponse +sendMLSCommitBundle remoteDomain msr = handleMLSMessageErrors $ do + assertMLSEnabled + loc <- qualifyLocal () + let sender = toRemoteUnsafe remoteDomain msr.sender + bundle <- + either (throw . mlsProtocolError) pure $ + decodeMLS' (fromBase64ByteString msr.rawMessage) + + ibundle <- noteS @'MLSUnsupportedMessage $ mkIncomingBundle bundle + (ctype, qConvOrSub) <- getConvFromGroupId ibundle.groupId + when (qUnqualified qConvOrSub /= msr.convOrSubId) $ throwS @'MLSGroupConversationMismatch + + -- this cannot throw the error since we always pass the sender which is qualified to be remote + runInputConst (fromMaybe def msr.enableOutOfSyncCheck) $ + MLSMessageResponseUpdates + . fmap lcuUpdate + <$> mapToRuntimeError @MLSLegalholdIncompatible + (InternalErrorWithDescription "expected group conversation while handling policy conflicts") + ( postMLSCommitBundle + loc + -- Type application to prevent future changes from introducing errors. + -- It is only safe to assume that we can discard the error when the sender + -- is actually remote. + -- Since `tUntagged` works on local and remote, a future changed may + -- go unchecked without this. + (tUntagged @QRemote sender) + msr.senderClient + ctype + qConvOrSub + Nothing + ibundle + ) + +sendMLSMessage :: + ( Member BackendNotificationQueueAccess r, + Member BrigAPIAccess r, + Member E.ConversationStore r, + Member ExternalAccess r, + Member (Error FederationError) r, + Member (Error InternalError) r, + Member (FederationAPIAccess FederatorClient) r, + Member NotificationSubsystem r, + Member (Input (Local ())) r, + Member (Input (Maybe (MLSKeysByPurpose MLSPrivateKeys))) r, + Member Now r, + Member LegalHoldStore r, + Member P.TinyLog r, + Member ProposalStore r, + Member TeamCollaboratorsSubsystem r, + Member TeamStore r + ) => + Domain -> + MLSMessageSendRequest -> + Sem r MLSMessageResponse +sendMLSMessage remoteDomain msr = handleMLSMessageErrors $ do + assertMLSEnabled + loc <- qualifyLocal () + let sender = toRemoteUnsafe remoteDomain msr.sender + raw <- either (throw . mlsProtocolError) pure $ decodeMLS' (fromBase64ByteString msr.rawMessage) + msg <- noteS @'MLSUnsupportedMessage $ mkIncomingMessage raw + (ctype, qConvOrSub) <- getConvFromGroupId msg.groupId + when (qUnqualified qConvOrSub /= msr.convOrSubId) $ throwS @'MLSGroupConversationMismatch + runInputConst (fromMaybe def msr.enableOutOfSyncCheck) $ + MLSMessageResponseUpdates . map lcuUpdate + <$> postMLSMessage + loc + (tUntagged sender) + msr.senderClient + ctype + qConvOrSub + Nothing + msg + +getSubConversationForRemoteUser :: + ( Member E.ConversationStore r, + Member (Input (Local ())) r, + Member TeamSubsystem r + ) => + Domain -> + GetSubConversationsRequest -> + Sem r GetSubConversationsResponse +getSubConversationForRemoteUser domain GetSubConversationsRequest {..} = + fmap (either GetSubConversationsResponseError GetSubConversationsResponseSuccess) + . runError @GalleyError + . mapToGalleyError @MLSGetSubConvStaticErrors + $ do + let qusr = Qualified gsreqUser domain + lconv <- qualifyLocal gsreqConv + getLocalSubConversation qusr lconv gsreqSubConv + +leaveSubConversation :: + ( HasLeaveSubConversationEffects r, + Member (Error FederationError) r, + Member (Input (Local ())) r, + Member Resource r, + Member TeamSubsystem r, + Member E.MLSCommitLockStore r, + Member (Input ConversationSubsystemConfig) r + ) => + Domain -> + LeaveSubConversationRequest -> + Sem r LeaveSubConversationResponse +leaveSubConversation domain lscr = do + let rusr = toRemoteUnsafe domain (lscrUser lscr) + cid = mkClientIdentity (tUntagged rusr) (lscrClient lscr) + lcnv <- qualifyLocal (lscrConv lscr) + fmap (either (LeaveSubConversationResponseProtocolError . unTagged) Imports.id) + . runError @MLSProtocolError + . fmap (either LeaveSubConversationResponseError Imports.id) + . runError @GalleyError + . mapToGalleyError @LeaveSubConversationStaticErrors + $ leaveLocalSubConversation cid lcnv (lscrSubConv lscr) + $> LeaveSubConversationResponseOk + +deleteSubConversationForRemoteUser :: + ( Member E.ConversationStore r, + Member (Input (Local ())) r, + Member Resource r, + Member TeamSubsystem r, + Member E.MLSCommitLockStore r + ) => + Domain -> + DeleteSubConversationFedRequest -> + Sem r DeleteSubConversationResponse +deleteSubConversationForRemoteUser domain DeleteSubConversationFedRequest {..} = + fmap + ( either + DeleteSubConversationResponseError + (\() -> DeleteSubConversationResponseSuccess) + ) + . runError @GalleyError + . mapToGalleyError @MLSDeleteSubConvStaticErrors + $ do + let qusr = Qualified dscreqUser domain + dsc = MLSReset dscreqGroupId dscreqEpoch + lconv <- qualifyLocal dscreqConv + resetLocalSubConversation qusr lconv dscreqSubConv dsc + +getOne2OneConversationV1 :: + ( Member (Input (Local ())) r, + Member BrigAPIAccess r, + Member (Error InvalidInput) r + ) => + Domain -> + GetOne2OneConversationRequest -> + Sem r GetOne2OneConversationResponse +getOne2OneConversationV1 domain (GetOne2OneConversationRequest self other) = + fmap (Imports.fromRight GetOne2OneConversationNotConnected) + . runError @(Tagged 'NotConnected ()) + $ do + lother <- qualifyLocal other + let rself = toRemoteUnsafe domain self + ensureConnectedToRemotes lother [rself] + foldQualified + lother + (const . throw $ FederationFunctionNotSupported "Getting 1:1 conversations is not supported over federation API < V2.") + (const (pure GetOne2OneConversationBackendMismatch)) + (one2OneConvId BaseProtocolMLSTag (tUntagged lother) (tUntagged rself)) + +getOne2OneConversation :: + ( Member E.ConversationStore r, + Member (Input (Local ())) r, + Member (Error InternalError) r, + Member BrigAPIAccess r, + Member (Input (Maybe (MLSKeysByPurpose MLSPrivateKeys))) r + ) => + Domain -> + GetOne2OneConversationRequest -> + Sem r GetOne2OneConversationResponseV2 +getOne2OneConversation domain (GetOne2OneConversationRequest self other) = + fmap (Imports.fromRight GetOne2OneConversationV2MLSNotEnabled) + . runError @(Tagged 'MLSNotEnabled ()) + . fmap (Imports.fromRight GetOne2OneConversationV2NotConnected) + . runError @(Tagged 'NotConnected ()) + $ do + lother <- qualifyLocal other + let rself = toRemoteUnsafe domain self + let getLocal lconv = do + mconv <- E.getConversation (tUnqualified lconv) + mlsPublicKeys <- mlsKeysToPublic <$$> getMLSPrivateKeys + conv <- case mconv of + Nothing -> pure (localMLSOne2OneConversationAsRemote lconv) + Just conv -> + note + (InternalErrorWithDescription "Unexpected member list in 1-1 conversation") + (conversationToRemote (tDomain lother) rself conv) + pure . GetOne2OneConversationV2Ok $ RemoteMLSOne2OneConversation conv mlsPublicKeys + + ensureConnectedToRemotes lother [rself] + + foldQualified + lother + getLocal + (const (pure GetOne2OneConversationV2BackendMismatch)) + (one2OneConvId BaseProtocolMLSTag (tUntagged lother) (tUntagged rself)) + +-------------------------------------------------------------------------------- +-- Error handling machinery + +class ToGalleyRuntimeError (effs :: EffectRow) r where + mapToGalleyError :: + (Member (Error GalleyError) r) => + Sem (Append effs r) a -> + Sem r a + +instance ToGalleyRuntimeError '[] r where + mapToGalleyError = Imports.id + +instance + forall (err :: GalleyError) effs r. + ( ToGalleyRuntimeError effs r, + SingI err, + Member (Error GalleyError) (Append effs r) + ) => + ToGalleyRuntimeError (ErrorS err ': effs) r + where + mapToGalleyError act = + mapToGalleyError @effs @r $ + runError act >>= \case + Left _ -> throw (demote @err) + Right res -> pure res + +onMLSMessageSent :: + ( Member ExternalAccess r, + Member NotificationSubsystem r, + Member (Input (Local ())) r, + Member (Input (Maybe (MLSKeysByPurpose MLSPrivateKeys))) r, + Member E.ConversationStore r, + Member P.TinyLog r + ) => + Domain -> + RemoteMLSMessage -> + Sem r EmptyResponse +onMLSMessageSent domain rmm = + (EmptyResponse <$) + . (logError =<<) + . runError @(Tagged 'MLSNotEnabled ()) + $ do + assertMLSEnabled + loc <- qualifyLocal () + let rcnv = toRemoteUnsafe domain rmm.conversation + let users = Map.keys rmm.recipients + (members, allMembers) <- + first Set.fromList + <$> E.selectRemoteMembers (toList users) rcnv + unless allMembers $ + P.warn $ + Log.field "conversation" (toByteString' (tUnqualified rcnv)) + Log.~~ Log.field "domain" (toByteString' (tDomain rcnv)) + Log.~~ Log.msg + ( "Attempt to send remote message to local\ + \ users not in the conversation" :: + ByteString + ) + let recipients = + filter (\r -> Set.member (recipientUserId r) members) + . map (\(u, clts) -> Recipient u (RecipientClientsSome clts)) + . Map.assocs + $ rmm.recipients + -- FUTUREWORK: support local bots + let e = + Event (tUntagged rcnv) rmm.subConversation (EventFromUser rmm.sender) rmm.time Nothing $ + EdMLSMessage (fromBase64ByteString rmm.message) + + runMessagePush loc (Just (tUntagged rcnv)) $ + newMessagePush mempty Nothing rmm.metadata recipients e + where + logError :: (Member P.TinyLog r) => Either (Tagged 'MLSNotEnabled ()) () -> Sem r () + logError (Left _) = + P.warn $ + Log.field "conversation" (toByteString' rmm.conversation) + Log.~~ Log.field "domain" (toByteString' domain) + Log.~~ Log.msg + ("Cannot process remote MLS message because MLS is disabled on this backend" :: ByteString) + logError _ = pure () + +mlsSendWelcome :: + ( Member (Error InternalError) r, + Member NotificationSubsystem r, + Member ExternalAccess r, + Member P.TinyLog r, + Member (Input (Maybe (MLSKeysByPurpose MLSPrivateKeys))) r, + Member (Input (Local ())) r, + Member Now r + ) => + Domain -> + MLSWelcomeRequest -> + Sem r MLSWelcomeResponse +mlsSendWelcome origDomain req = do + fmap (either (const MLSWelcomeMLSNotEnabled) (const MLSWelcomeSent)) + . runError @(Tagged 'MLSNotEnabled ()) + $ do + assertMLSEnabled + loc <- qualifyLocal () + now <- Now.get + welcome <- + either (throw . InternalErrorWithDescription . LT.fromStrict) pure $ + decodeMLS' (fromBase64ByteString req.welcomeMessage) + sendLocalWelcomes req.qualifiedConvId (Qualified req.originatingUser origDomain) Nothing now welcome (qualifyAs loc req.recipients) + +queryGroupInfo :: + ( Member E.ConversationStore r, + Member (Input (Local ())) r, + Member (Input (Maybe (MLSKeysByPurpose MLSPrivateKeys))) r + ) => + Domain -> + GetGroupInfoRequest -> + Sem r GetGroupInfoResponse +queryGroupInfo origDomain req = + fmap (either GetGroupInfoResponseError GetGroupInfoResponseState) + . runError @GalleyError + . mapToGalleyError @MLSGroupInfoStaticErrors + $ do + assertMLSEnabled + let sender = toRemoteUnsafe origDomain . (.sender) $ req + state <- case req.conv of + Conv convId -> do + lconvId <- qualifyLocal convId + getGroupInfoFromLocalConv (tUntagged sender) lconvId + SubConv convId subConvId -> do + lconvId <- qualifyLocal convId + getSubConversationGroupInfoFromLocalConv (tUntagged sender) subConvId lconvId + pure + . Base64ByteString + . unGroupInfoData + $ state + +updateTypingIndicator :: + ( Member NotificationSubsystem r, + Member (FederationAPIAccess FederatorClient) r, + Member E.ConversationStore r, + Member Now r, + Member (Input (Local ())) r, + Member TeamSubsystem r + ) => + Domain -> + TypingDataUpdateRequest -> + Sem r TypingDataUpdateResponse +updateTypingIndicator origDomain TypingDataUpdateRequest {..} = do + let qusr = Qualified userId origDomain + lcnv <- qualifyLocal convId + + ret <- runError + . mapToRuntimeError @'ConvNotFound ConvNotFound + $ do + conv <- maskConvAccessDenied $ getConversationAsMember qusr lcnv + notifyTypingIndicator conv qusr Nothing typingStatus + + pure (either TypingDataUpdateError TypingDataUpdateSuccess ret) + +onTypingIndicatorUpdated :: + (Member NotificationSubsystem r) => + Domain -> + TypingDataUpdated -> + Sem r EmptyResponse +onTypingIndicatorUpdated origDomain TypingDataUpdated {..} = do + let qcnv = Qualified convId origDomain + pushTypingIndicatorEvents origUserId time usersInConv Nothing qcnv typingStatus + pure EmptyResponse + +-------------------------------------------------------------------------------- +-- Utilities +-------------------------------------------------------------------------------- + +-- | Log a federation error that is impossible in processing a remote request +-- for a local conversation. +logFederationError :: + (Member P.TinyLog r) => + Local ConvId -> + FederationError -> + Sem r () +logFederationError lc e = + P.warn $ + Log.field "conversation" (toByteString' (tUnqualified lc)) + Log.~~ Log.field "domain" (toByteString' (tDomain lc)) + Log.~~ Log.msg + ( "An impossible federation error occurred when deleting\ + \ a user from a local conversation: " + <> displayException e + ) diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index d1e146b7537..67493be4915 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -51,8 +51,6 @@ import Galley.API.Teams.Features import Galley.API.Teams.Features.Get import Galley.API.Update qualified as Update import Galley.App -import Galley.Effects -import Galley.Effects.CustomBackendStore import Galley.Monad import Galley.Options hiding (brig) import Galley.Queue qualified as Q @@ -76,6 +74,7 @@ import Wire.API.Event.LeaveReason import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Error +import Wire.API.MLS.Keys (MLSKeysByPurpose, MLSPrivateKeys) import Wire.API.Push.V2 qualified as PushV2 import Wire.API.Routes.API import Wire.API.Routes.Internal.Brig.EJPD @@ -88,20 +87,26 @@ import Wire.API.Team.FeatureFlags (FanoutLimit) import Wire.API.User (UserIds (cUsers)) import Wire.API.User.Client import Wire.BackendNotificationQueueAccess +import Wire.BrigAPIAccess (BrigAPIAccess) import Wire.ConversationStore -import Wire.ConversationStore qualified as E +import Wire.ConversationStore qualified as ConversationStore import Wire.ConversationStore.MLS.Types import Wire.ConversationSubsystem import Wire.ConversationSubsystem.One2One import Wire.ConversationSubsystem.Util +import Wire.CustomBackendStore +import Wire.ExternalAccess (ExternalAccess) import Wire.FeaturesConfigSubsystem (FeaturesConfigSubsystem) import Wire.FederationSubsystem (getFederationStatus) import Wire.LegalHoldStore as LegalHoldStore +import Wire.ListItems import Wire.NotificationSubsystem +import Wire.ProposalStore (ProposalStore) import Wire.Sem.Now (Now) import Wire.Sem.Now qualified as Now import Wire.Sem.Paging import Wire.Sem.Paging.Cassandra +import Wire.Sem.Random (Random) import Wire.ServiceStore import Wire.StoredConversation import Wire.StoredConversation qualified as Data @@ -145,9 +150,10 @@ iEJPDAPI = mkNamedAPI @"get-conversations-by-user" ejpdGetConvInfo ejpdGetConvInfo :: forall r. ( Member ConversationStore r, + Member ConversationSubsystem r, Member (Error InternalError) r, Member (Input (Local ())) r, - Member (Input Env) r, + Member (Input (Maybe (MLSKeysByPurpose MLSPrivateKeys))) r, Member P.TinyLog r ) => UserId -> @@ -174,7 +180,7 @@ ejpdGetConvInfo uid = do One2OneConv -> Nothing SelfConv -> Nothing ConnectConv -> Nothing - renderedPage <- mapMaybe mk <$> getConversations (fst $ partitionQualified luid convids) + renderedPage <- mapMaybe mk <$> ConversationStore.getConversations (fst $ partitionQualified luid convids) if MTP.mtpHasMore page then do newPage <- Query.conversationIdsPageFrom luid (mkPageRequest . MTP.mtpPagingState $ page) @@ -196,7 +202,7 @@ conversationAPI = <@> mkNamedAPI @"conversation-mls-one-to-one" Query.getMLSOne2OneConversationInternal <@> mkNamedAPI @"conversation-mls-one-to-one-established" Query.isMLSOne2OneEstablished <@> mkNamedAPI @"get-conversation-by-id" Query.getLocalConversationInternal - <@> mkNamedAPI @"is-conversation-out-of-sync" E.isConversationOutOfSync + <@> mkNamedAPI @"is-conversation-out-of-sync" ConversationStore.isConversationOutOfSync legalholdWhitelistedTeamsAPI :: API ILegalholdWhitelistedTeamsAPI GalleyEffects legalholdWhitelistedTeamsAPI = mkAPI $ \tid -> hoistAPIHandler Imports.id (base tid) @@ -357,7 +363,7 @@ rmUser :: Member ExternalAccess r, Member NotificationSubsystem r, Member ConversationSubsystem r, - Member (Input Env) r, + Member (Input (Maybe (MLSKeysByPurpose MLSPrivateKeys))) r, Member Now r, Member (ListItems p2 TeamId) r, Member ProposalStore r, @@ -424,18 +430,18 @@ rmUser lusr conn = do leaveLocalConversations :: [ConvId] -> Sem r () leaveLocalConversations ids = do let qUser = tUntagged lusr - cc <- getConversations ids + cc <- ConversationStore.getConversations ids now <- Now.get pp <- for cc $ \c -> case Data.convType c of SelfConv -> pure Nothing - One2OneConv -> E.deleteMembers c.id_ (UserList [tUnqualified lusr] []) $> Nothing - ConnectConv -> E.deleteMembers c.id_ (UserList [tUnqualified lusr] []) $> Nothing + One2OneConv -> ConversationStore.deleteMembers c.id_ (UserList [tUnqualified lusr] []) $> Nothing + ConnectConv -> ConversationStore.deleteMembers c.id_ (UserList [tUnqualified lusr] []) $> Nothing RegularConv | tUnqualified lusr `isMember` c.localMembers -> do runError (removeUser (qualifyAs lusr c) RemoveUserIncludeMain (tUntagged lusr)) >>= \case Left e -> P.err $ Log.msg ("failed to send remove proposal: " <> internalErrorDescription e) Right _ -> pure () - E.deleteMembers c.id_ (UserList [tUnqualified lusr] []) + ConversationStore.deleteMembers c.id_ (UserList [tUnqualified lusr] []) let e = Event { evtConv = tUntagged (qualifyAs lusr c.id_), @@ -535,5 +541,5 @@ iGetMLSClientListForConv :: GroupId -> Sem r ClientList iGetMLSClientListForConv gid = do - cm <- E.lookupMLSClients gid + cm <- ConversationStore.lookupMLSClients gid pure $ ClientList (concatMap (Map.keys . snd) (Map.assocs (unClientMap cm))) diff --git a/services/galley/src/Galley/API/LegalHold.hs b/services/galley/src/Galley/API/LegalHold.hs index 322ae323194..c219fca6085 100644 --- a/services/galley/src/Galley/API/LegalHold.hs +++ b/services/galley/src/Galley/API/LegalHold.hs @@ -45,9 +45,6 @@ import Galley.API.LegalHold.Get import Galley.API.LegalHold.Team import Galley.API.Query (iterateConversations) import Galley.API.Update (removeMemberFromLocalConv) -import Galley.App -import Galley.Effects -import Galley.Effects.TeamMemberStore import Galley.External.LegalHoldService qualified as LHService import Galley.Types.Error import Imports @@ -63,7 +60,6 @@ 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 @@ -76,21 +72,24 @@ 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.BackendNotificationQueueAccess import Wire.BrigAPIAccess -import Wire.ConversationStore +import Wire.ConversationStore (ConversationStore) import Wire.ConversationSubsystem import Wire.ConversationSubsystem.Util +import Wire.ExternalAccess (ExternalAccess) import Wire.FeaturesConfigSubsystem -import Wire.FederationSubsystem (FederationSubsystem) import Wire.FireAndForget import Wire.LegalHoldStore qualified as LegalHoldData import Wire.NotificationSubsystem +import Wire.ProposalStore (ProposalStore) import Wire.Sem.Now (Now) import Wire.Sem.Paging import Wire.Sem.Paging.Cassandra +import Wire.Sem.Random (Random) import Wire.StoredConversation import Wire.StoredConversation qualified as Data -import Wire.TeamCollaboratorsSubsystem +import Wire.TeamMemberStore import Wire.TeamStore import Wire.TeamSubsystem (TeamSubsystem) import Wire.TeamSubsystem qualified as TeamSubsystem @@ -102,7 +101,7 @@ createSettings :: Member (ErrorS 'LegalHoldNotEnabled) r, Member (ErrorS 'LegalHoldServiceInvalidKey) r, Member (ErrorS 'LegalHoldServiceBadResponse) r, - Member LegalHoldStore r, + Member LegalHoldData.LegalHoldStore r, Member P.TinyLog r, Member (Input (FeatureDefaults LegalholdConfig)) r, Member TeamSubsystem r, @@ -132,7 +131,7 @@ createSettings lzusr tid newService = do getSettings :: forall r. ( Member (ErrorS 'NotATeamMember) r, - Member LegalHoldStore r, + Member LegalHoldData.LegalHoldStore r, Member (Input (FeatureDefaults LegalholdConfig)) r, Member TeamSubsystem r, Member FeaturesConfigSubsystem r @@ -168,24 +167,19 @@ removeSettingsInternalPaging :: Member (ErrorS OperationDenied) r, Member (ErrorS 'UserLegalHoldIllegalOperation) r, Member ExternalAccess r, - Member (FederationAPIAccess FederatorClient) r, Member FireAndForget r, Member NotificationSubsystem r, Member ConversationSubsystem r, - Member (Input Env) r, Member (Input (Local ())) r, Member Now r, - Member LegalHoldStore r, + Member LegalHoldData.LegalHoldStore r, Member ProposalStore r, Member P.TinyLog r, Member Random r, Member (TeamMemberStore InternalPaging) r, Member TeamStore r, Member (Embed IO) r, - Member TeamCollaboratorsSubsystem r, - Member MLSCommitLockStore r, Member (Input (FeatureDefaults LegalholdConfig)) r, - Member FederationSubsystem r, Member TeamSubsystem r, Member (Input ConversationSubsystemConfig) r, Member FeaturesConfigSubsystem r @@ -217,22 +211,17 @@ removeSettings :: Member (ErrorS OperationDenied) r, Member (ErrorS 'UserLegalHoldIllegalOperation) r, Member ExternalAccess r, - Member (FederationAPIAccess FederatorClient) r, Member FireAndForget r, Member NotificationSubsystem r, Member ConversationSubsystem r, - Member (Input Env) r, Member (Input (Local ())) r, Member Now r, - Member LegalHoldStore r, + Member LegalHoldData.LegalHoldStore r, Member ProposalStore r, Member P.TinyLog r, Member Random r, Member (Embed IO) r, - Member TeamCollaboratorsSubsystem r, - Member MLSCommitLockStore r, Member (Input (FeatureDefaults LegalholdConfig)) r, - Member FederationSubsystem r, Member TeamSubsystem r, Member (Input ConversationSubsystemConfig) r, Member FeaturesConfigSubsystem r @@ -277,23 +266,18 @@ removeSettings' :: Member (ErrorS 'UserLegalHoldIllegalOperation) r, Member (ErrorS 'LegalHoldCouldNotBlockConnections) r, Member ExternalAccess r, - Member (FederationAPIAccess FederatorClient) r, Member FireAndForget r, Member NotificationSubsystem r, Member ConversationSubsystem r, Member Now r, Member (Input (Local ())) r, - Member (Input Env) r, - Member LegalHoldStore r, + Member LegalHoldData.LegalHoldStore r, Member (TeamMemberStore p) r, Member TeamStore r, Member ProposalStore r, Member Random r, Member P.TinyLog r, Member (Embed IO) r, - Member TeamCollaboratorsSubsystem r, - Member MLSCommitLockStore r, - Member FederationSubsystem r, Member TeamSubsystem r, Member (Input ConversationSubsystemConfig) r ) => @@ -333,19 +317,14 @@ grantConsent :: Member (ErrorS 'TeamMemberNotFound) r, Member (ErrorS 'UserLegalHoldIllegalOperation) r, Member ExternalAccess r, - Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, - Member (Input Env) r, Member Now r, - Member LegalHoldStore r, + Member LegalHoldData.LegalHoldStore r, Member ProposalStore r, Member P.TinyLog r, Member Random r, Member TeamStore r, - Member TeamCollaboratorsSubsystem r, - Member MLSCommitLockStore r, - Member FederationSubsystem r, Member TeamSubsystem r, Member (Input ConversationSubsystemConfig) r ) => @@ -384,22 +363,17 @@ requestDevice :: Member (ErrorS 'UserLegalHoldAlreadyEnabled) r, Member (ErrorS 'UserLegalHoldIllegalOperation) r, Member ExternalAccess r, - Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, Member (Input (Local ())) r, - Member (Input Env) r, Member Now r, - Member LegalHoldStore r, + Member LegalHoldData.LegalHoldStore r, Member ProposalStore r, Member P.TinyLog r, Member Random r, Member TeamStore r, Member (Embed IO) r, - Member TeamCollaboratorsSubsystem r, - Member MLSCommitLockStore r, Member (Input (FeatureDefaults LegalholdConfig)) r, - Member FederationSubsystem r, Member TeamSubsystem r, Member (Input ConversationSubsystemConfig) r, Member FeaturesConfigSubsystem r @@ -482,22 +456,17 @@ approveDevice :: Member (ErrorS 'UserLegalHoldIllegalOperation) r, Member (ErrorS 'UserLegalHoldNotPending) r, Member ExternalAccess r, - Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, Member (Input (Local ())) r, - Member (Input Env) r, Member Now r, - Member LegalHoldStore r, + Member LegalHoldData.LegalHoldStore r, Member ProposalStore r, Member P.TinyLog r, Member Random r, Member TeamStore r, Member (Embed IO) r, - Member TeamCollaboratorsSubsystem r, - Member MLSCommitLockStore r, Member (Input (FeatureDefaults LegalholdConfig)) r, - Member FederationSubsystem r, Member TeamSubsystem r, Member (Input ConversationSubsystemConfig) r, Member FeaturesConfigSubsystem r @@ -564,21 +533,16 @@ disableForUser :: Member (ErrorS OperationDenied) r, Member (ErrorS 'UserLegalHoldIllegalOperation) r, Member ExternalAccess r, - Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, - Member (Input Env) r, Member (Input (Local ())) r, Member Now r, - Member LegalHoldStore r, + Member LegalHoldData.LegalHoldStore r, Member ProposalStore r, Member P.TinyLog r, Member Random r, Member TeamStore r, Member (Embed IO) r, - Member TeamCollaboratorsSubsystem r, - Member MLSCommitLockStore r, - Member FederationSubsystem r, Member TeamSubsystem r, Member (Input ConversationSubsystemConfig) r ) => @@ -633,19 +597,14 @@ changeLegalholdStatusAndHandlePolicyConflicts :: Member (ErrorS 'LegalHoldCouldNotBlockConnections) r, Member (ErrorS 'UserLegalHoldIllegalOperation) r, Member ExternalAccess r, - Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, - Member (Input Env) r, Member Now r, - Member LegalHoldStore r, + Member LegalHoldData.LegalHoldStore r, Member TeamStore r, Member ProposalStore r, Member Random r, Member P.TinyLog r, - Member TeamCollaboratorsSubsystem r, - Member MLSCommitLockStore r, - Member FederationSubsystem r, Member TeamSubsystem r, Member (Input ConversationSubsystemConfig) r ) => @@ -724,7 +683,7 @@ blockNonConsentingConnections uid = do status <- putConnectionInternal (BlockForMissingLHConsent userLegalhold othersToBlock) pure $ ["blocking users failed: " <> show (status, othersToBlock) | status /= status200] -unsetTeamLegalholdWhitelistedH :: (Member LegalHoldStore r) => TeamId -> Sem r () +unsetTeamLegalholdWhitelistedH :: (Member LegalHoldData.LegalHoldStore r) => TeamId -> Sem r () unsetTeamLegalholdWhitelistedH tid = do () <- error @@ -754,18 +713,13 @@ handleGroupConvPolicyConflicts :: Member (Error InternalError) r, Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, Member ExternalAccess r, - Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, - Member (Input Env) r, Member Now r, Member ProposalStore r, Member P.TinyLog r, Member Random r, Member TeamStore r, - Member TeamCollaboratorsSubsystem r, - Member MLSCommitLockStore r, - Member FederationSubsystem r, Member TeamSubsystem r, Member (Input ConversationSubsystemConfig) r ) => diff --git a/services/galley/src/Galley/API/LegalHold/Conflicts.hs b/services/galley/src/Galley/API/LegalHold/Conflicts.hs index 08857a060e5..c313d399c76 100644 --- a/services/galley/src/Galley/API/LegalHold/Conflicts.hs +++ b/services/galley/src/Galley/API/LegalHold/Conflicts.hs @@ -33,7 +33,6 @@ import Data.Map qualified as Map import Data.Misc import Data.Qualified import Data.Set qualified as Set -import Galley.Effects import Galley.Options import Imports import Polysemy diff --git a/services/galley/src/Galley/API/LegalHold/Get.hs b/services/galley/src/Galley/API/LegalHold/Get.hs index 37c90544250..bc3c036beb7 100644 --- a/services/galley/src/Galley/API/LegalHold/Get.hs +++ b/services/galley/src/Galley/API/LegalHold/Get.hs @@ -22,7 +22,6 @@ import Data.ByteString.Conversion (toByteString') import Data.Id import Data.LegalHold (UserLegalHoldStatus (..)) import Data.Qualified -import Galley.Effects import Galley.Types.Error import Imports import Polysemy @@ -45,7 +44,7 @@ getUserStatus :: forall r. ( Member (Error InternalError) r, Member (ErrorS 'TeamMemberNotFound) r, - Member LegalHoldStore r, + Member LegalHoldData.LegalHoldStore r, Member P.TinyLog r, Member TeamSubsystem r ) => diff --git a/services/galley/src/Galley/API/LegalHold/Team.hs b/services/galley/src/Galley/API/LegalHold/Team.hs index 249d7722c0d..c7f511d4491 100644 --- a/services/galley/src/Galley/API/LegalHold/Team.hs +++ b/services/galley/src/Galley/API/LegalHold/Team.hs @@ -26,7 +26,6 @@ where import Data.Id import Data.Range -import Galley.Effects import Imports import Polysemy import Polysemy.Input (Input, input) @@ -38,6 +37,7 @@ import Wire.API.Team.Size import Wire.BrigAPIAccess import Wire.FeaturesConfigSubsystem (FeaturesConfigSubsystem, getDbFeatureRawInternal) import Wire.LegalHold +import Wire.LegalHoldStore (LegalHoldStore) assertLegalHoldEnabledForTeam :: forall r. diff --git a/services/galley/src/Galley/API/MLS.hs b/services/galley/src/Galley/API/MLS.hs index 0d0d5abe601..8da6d00620c 100644 --- a/services/galley/src/Galley/API/MLS.hs +++ b/services/galley/src/Galley/API/MLS.hs @@ -29,7 +29,6 @@ where import Data.Default import Galley.API.MLS.Enabled import Galley.API.MLS.Message -import Galley.Env import Galley.Types.Error import Imports import Polysemy @@ -40,7 +39,7 @@ import Wire.API.Error.Galley import Wire.API.MLS.Keys getMLSPublicKeys :: - ( Member (Input Env) r, + ( Member (Input (Maybe (MLSKeysByPurpose MLSPrivateKeys))) r, Member (ErrorS 'MLSNotEnabled) r, Member (Error InternalError) r ) => diff --git a/services/galley/src/Galley/API/MLS/CheckClients.hs b/services/galley/src/Galley/API/MLS/CheckClients.hs index 9a834fd708d..22ff3de4813 100644 --- a/services/galley/src/Galley/API/MLS/CheckClients.hs +++ b/services/galley/src/Galley/API/MLS/CheckClients.hs @@ -30,7 +30,6 @@ import Data.Qualified import Data.Set qualified as Set import Data.Tuple.Extra import Galley.API.MLS.Commit.Core -import Galley.Effects import Imports import Polysemy import Polysemy.Error @@ -42,7 +41,9 @@ import Wire.API.MLS.CipherSuite import Wire.API.MLS.KeyPackage import Wire.API.MLS.LeafNode import Wire.API.User.Client +import Wire.BrigAPIAccess (BrigAPIAccess) import Wire.ConversationStore.MLS.Types +import Wire.FederationAPIAccess (FederationAPIAccess) checkClients :: ( Member BrigAPIAccess r, diff --git a/services/galley/src/Galley/API/MLS/Commit.hs b/services/galley/src/Galley/API/MLS/Commit.hs deleted file mode 100644 index 39088273b8b..00000000000 --- a/services/galley/src/Galley/API/MLS/Commit.hs +++ /dev/null @@ -1,28 +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.API.MLS.Commit - ( getCommitData, - getExternalCommitData, - processInternalCommit, - processExternalCommit, - ) -where - -import Galley.API.MLS.Commit.Core -import Galley.API.MLS.Commit.ExternalCommit -import Galley.API.MLS.Commit.InternalCommit diff --git a/services/galley/src/Galley/API/MLS/Commit/Core.hs b/services/galley/src/Galley/API/MLS/Commit/Core.hs index 124a1c4d5b8..0318881e606 100644 --- a/services/galley/src/Galley/API/MLS/Commit/Core.hs +++ b/services/galley/src/Galley/API/MLS/Commit/Core.hs @@ -34,9 +34,6 @@ import Data.Qualified import Galley.API.MLS.Conversation import Galley.API.MLS.IncomingMessage import Galley.API.MLS.Proposal -import Galley.Effects -import Galley.Env -import Galley.Options import Galley.Types.Error import Imports import Polysemy @@ -65,13 +62,19 @@ import Wire.API.MLS.SubConversation import Wire.API.MLS.Validation import Wire.API.MLS.Validation.Error (toText) import Wire.API.User.Client +import Wire.BackendNotificationQueueAccess import Wire.BrigAPIAccess import Wire.ConversationStore import Wire.ConversationStore.MLS.Types +import Wire.ExternalAccess import Wire.FederationAPIAccess +import Wire.LegalHoldStore (LegalHoldStore) import Wire.NotificationSubsystem +import Wire.ProposalStore (ProposalStore) import Wire.Sem.Now (Now) +import Wire.Sem.Random (Random) import Wire.TeamCollaboratorsSubsystem +import Wire.TeamStore type HasProposalActionEffects r = ( Member BackendNotificationQueueAccess r, @@ -91,8 +94,6 @@ type HasProposalActionEffects r = Member ExternalAccess r, Member (FederationAPIAccess FederatorClient) r, Member (Input ConversationSubsystemConfig) r, - Member (Input Env) r, - Member (Input Opts) r, Member Now r, Member LegalHoldStore r, Member ProposalStore r, diff --git a/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs b/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs index c8f2bacd57c..9aec3fa0ea1 100644 --- a/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs +++ b/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs @@ -33,7 +33,6 @@ import Galley.API.MLS.IncomingMessage import Galley.API.MLS.Proposal import Galley.API.MLS.Removal import Galley.API.MLS.Util -import Galley.Effects import Imports import Polysemy import Polysemy.Error diff --git a/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs b/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs index e2152e2ebb5..53c51cfa62a 100644 --- a/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs +++ b/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs @@ -37,7 +37,6 @@ import Galley.API.MLS.IncomingMessage import Galley.API.MLS.One2One import Galley.API.MLS.Proposal import Galley.API.MLS.Util -import Galley.Effects import Galley.Types.Error import Imports import Polysemy @@ -65,6 +64,7 @@ import Wire.ConversationSubsystem import Wire.ConversationSubsystem.Util import Wire.FederationSubsystem import Wire.ProposalStore +import Wire.Sem.Random (Random) import Wire.StoredConversation import Wire.TeamSubsystem (TeamSubsystem) @@ -259,7 +259,6 @@ processInternalCommit senderIdentity con lConvOrSub ciphersuite ciphersuiteUpdat addMembers :: ( HasProposalActionEffects r, Member ConversationSubsystem r, - Member MLSCommitLockStore r, Member FederationSubsystem r, Member TeamSubsystem r ) => @@ -276,7 +275,7 @@ addMembers qusr con lConvOrSub users = case tUnqualified lConvOrSub of ( handleNoChanges . handleMLSProposalFailures @ProposalErrors . fmap pure - . updateLocalConversationUnchecked @'ConversationJoinTag lconv qusr con + . updateLocalConversationUncheckedJoin lconv qusr con . (\uids -> ConversationJoin uids roleNameWireMember def) ) . nonEmpty @@ -288,8 +287,6 @@ addMembers qusr con lConvOrSub users = case tUnqualified lConvOrSub of removeMembers :: ( HasProposalActionEffects r, Member ConversationSubsystem r, - Member MLSCommitLockStore r, - Member FederationSubsystem r, Member TeamSubsystem r ) => Qualified UserId -> @@ -304,7 +301,7 @@ removeMembers qusr con lConvOrSub users = case tUnqualified lConvOrSub of ( handleNoChanges . handleMLSProposalFailures @ProposalErrors . fmap pure - . updateLocalConversationUnchecked @'ConversationRemoveMembersTag lconv qusr con + . updateLocalConversationUncheckedRemoveMembers lconv qusr con . flip ConversationRemoveMembers EdReasonRemoved ) . nonEmpty diff --git a/services/galley/src/Galley/API/MLS/Enabled.hs b/services/galley/src/Galley/API/MLS/Enabled.hs index ec1bd099baa..158d511e291 100644 --- a/services/galley/src/Galley/API/MLS/Enabled.hs +++ b/services/galley/src/Galley/API/MLS/Enabled.hs @@ -17,30 +17,28 @@ module Galley.API.MLS.Enabled where -import Control.Lens (view) -import Galley.Env import Imports hiding (getFirst) import Polysemy import Polysemy.Input import Wire.API.Error import Wire.API.Error.Galley -import Wire.API.MLS.Keys +import Wire.API.MLS.Keys (MLSKeysByPurpose, MLSPrivateKeys) -isMLSEnabled :: (Member (Input Env) r) => Sem r Bool -isMLSEnabled = inputs (isJust . view mlsKeys) +isMLSEnabled :: (Member (Input (Maybe (MLSKeysByPurpose MLSPrivateKeys))) r) => Sem r Bool +isMLSEnabled = inputs (isJust) -- | Fail if MLS is not enabled. Only use this function at the beginning of an -- MLS endpoint, NOT in utility functions. assertMLSEnabled :: - ( Member (Input Env) r, + ( Member (Input (Maybe (MLSKeysByPurpose MLSPrivateKeys))) r, Member (ErrorS 'MLSNotEnabled) r ) => Sem r () assertMLSEnabled = void getMLSPrivateKeys getMLSPrivateKeys :: - ( Member (Input Env) r, + ( Member (Input (Maybe (MLSKeysByPurpose MLSPrivateKeys))) r, Member (ErrorS 'MLSNotEnabled) r ) => Sem r (MLSKeysByPurpose MLSPrivateKeys) -getMLSPrivateKeys = noteS @'MLSNotEnabled =<< inputs (view mlsKeys) +getMLSPrivateKeys = noteS @'MLSNotEnabled =<< input diff --git a/services/galley/src/Galley/API/MLS/GroupInfo.hs b/services/galley/src/Galley/API/MLS/GroupInfo.hs index f09a21db861..69f667cab77 100644 --- a/services/galley/src/Galley/API/MLS/GroupInfo.hs +++ b/services/galley/src/Galley/API/MLS/GroupInfo.hs @@ -15,15 +15,19 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Galley.API.MLS.GroupInfo where +module Galley.API.MLS.GroupInfo + ( MLSGroupInfoStaticErrors, + getGroupInfo, + getGroupInfoFromLocalConv, + getGroupInfoFromRemoteConv, + ) +where import Data.Id as Id import Data.Json.Util import Data.Qualified import Galley.API.MLS.Enabled import Galley.API.MLS.Util -import Galley.Effects -import Galley.Env import Imports import Polysemy import Polysemy.Error @@ -35,6 +39,7 @@ 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.Keys (MLSKeysByPurpose, MLSPrivateKeys) import Wire.API.MLS.SubConversation import Wire.ConversationStore qualified as E import Wire.ConversationSubsystem.Util @@ -47,12 +52,12 @@ type MLSGroupInfoStaticErrors = ] getGroupInfo :: - ( Member ConversationStore r, + ( Member (Input (Maybe (MLSKeysByPurpose MLSPrivateKeys))) r, + Member E.ConversationStore r, Member (Error FederationError) r, - Member (FederationAPIAccess FederatorClient) r, - Member (Input Env) r + Member (E.FederationAPIAccess FederatorClient) r, + Members MLSGroupInfoStaticErrors r ) => - (Members MLSGroupInfoStaticErrors r) => Local UserId -> Qualified ConvId -> Sem r GroupInfoData @@ -65,7 +70,7 @@ getGroupInfo lusr qcnvId = do qcnvId getGroupInfoFromLocalConv :: - (Member ConversationStore r) => + (Member E.ConversationStore r) => (Members MLSGroupInfoStaticErrors r) => Qualified UserId -> Local ConvId -> @@ -77,7 +82,7 @@ getGroupInfoFromLocalConv qusr lcnvId = do getGroupInfoFromRemoteConv :: ( Member (Error FederationError) r, - Member (FederationAPIAccess FederatorClient) r + Member (E.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 798345adf3a..667a29dc3fe 100644 --- a/services/galley/src/Galley/API/MLS/GroupInfoCheck.hs +++ b/services/galley/src/Galley/API/MLS/GroupInfoCheck.hs @@ -18,15 +18,13 @@ module Galley.API.MLS.GroupInfoCheck ( checkGroupState, GroupInfoMismatch (..), + GroupInfoCheckEnabled (..), ) where -import Control.Lens (view) import Data.Bifunctor import Data.Id import Galley.API.Teams.Features.Get -import Galley.Effects -import Galley.Options import Imports import Polysemy import Polysemy.Error @@ -51,10 +49,14 @@ data GroupInfoMismatch = GroupInfoMismatch {clients :: [(Int, ClientIdentity)]} deriving (Show) +newtype GroupInfoCheckEnabled + = GroupInfoCheckEnabled Bool + deriving stock (Eq, Ord, Show) + checkGroupState :: forall r. ( Member (Error GroupInfoMismatch) r, - Member (Input Opts) r, + Member (Input (Maybe GroupInfoCheckEnabled)) r, Member (Error MLSProtocolError) r, Member ConversationStore r, Member FeaturesConfigSubsystem r @@ -104,13 +106,13 @@ existingGroupStateMismatch convOrSub = isGroupInfoCheckEnabled :: ( Member FeaturesConfigSubsystem r, - Member (Input Opts) r + Member (Input (Maybe GroupInfoCheckEnabled)) r ) => Maybe TeamId -> Sem r Bool isGroupInfoCheckEnabled Nothing = pure False isGroupInfoCheckEnabled (Just tid) = fmap isJust . runNonDetMaybe $ do - global <- inputs (view $ settings . checkGroupInfo) - guard (global == Just True) + global <- input + guard (global == Just (GroupInfoCheckEnabled True)) mls <- getFeatureForTeam @_ @MLSConfig tid guard (getAny mls.config.mlsGroupInfoDiagnostics) diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index e3868743bce..6425512192b 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -55,7 +55,6 @@ import Galley.API.MLS.Propagate import Galley.API.MLS.Proposal import Galley.API.MLS.Util import Galley.API.MLS.Welcome (sendWelcomes) -import Galley.Effects import Galley.Types.Error import Imports import Polysemy @@ -79,21 +78,25 @@ import Wire.API.MLS.Commit hiding (output) import Wire.API.MLS.CommitBundle import Wire.API.MLS.Credential import Wire.API.MLS.GroupInfo +import Wire.API.MLS.Keys (MLSKeysByPurpose, MLSPrivateKeys) import Wire.API.MLS.Message import Wire.API.MLS.OutOfSync import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation import Wire.API.Routes.Version import Wire.API.Team.LegalHold +import Wire.BrigAPIAccess (BrigAPIAccess) import Wire.ConversationStore import Wire.ConversationStore.MLS.Types import Wire.ConversationSubsystem import Wire.ConversationSubsystem.Util +import Wire.ExternalAccess import Wire.FeaturesConfigSubsystem import Wire.FederationAPIAccess import Wire.FederationSubsystem import Wire.NotificationSubsystem import Wire.Sem.Now qualified as Now +import Wire.Sem.Random (Random) import Wire.StoredConversation import Wire.TeamStore qualified as TeamStore import Wire.TeamSubsystem (TeamSubsystem) @@ -139,6 +142,7 @@ enableOutOfSyncCheckFromVersion v postMLSMessageFromLocalUser :: ( HasProposalEffects r, + Member (Input (Maybe (MLSKeysByPurpose MLSPrivateKeys))) r, Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'ConvMemberNotFound) r, Member (ErrorS 'ConvNotFound) r, @@ -180,6 +184,7 @@ postMLSCommitBundle :: Member (Error MLSOutOfSyncError) r, Member (ErrorS GroupIdVersionNotSupported) r, Member (Input EnableOutOfSyncCheck) r, + Member (Input (Maybe GroupInfoCheckEnabled)) r, Member Random r, Member Resource r, Members MLSBundleStaticErrors r, @@ -212,6 +217,8 @@ postMLSCommitBundleFromLocalUser :: Member (Error GroupInfoDiagnostics) r, Member (Error MLSOutOfSyncError) r, Member (ErrorS GroupIdVersionNotSupported) r, + Member (Input (Maybe GroupInfoCheckEnabled)) r, + Member (Input (Maybe (MLSKeysByPurpose MLSPrivateKeys))) r, Member Random r, Member Resource r, Members MLSBundleStaticErrors r, @@ -248,6 +255,7 @@ postMLSCommitBundleToLocalConv :: Member (Error MLSOutOfSyncError) r, Member (ErrorS GroupIdVersionNotSupported) r, Member (Input EnableOutOfSyncCheck) r, + Member (Input (Maybe GroupInfoCheckEnabled)) r, Member Random r, Member Resource r, Members MLSBundleStaticErrors r, diff --git a/services/galley/src/Galley/API/MLS/OutOfSync.hs b/services/galley/src/Galley/API/MLS/OutOfSync.hs index e12aaddf65e..03556f89ade 100644 --- a/services/galley/src/Galley/API/MLS/OutOfSync.hs +++ b/services/galley/src/Galley/API/MLS/OutOfSync.hs @@ -26,7 +26,6 @@ import Data.Map qualified as Map import Data.Qualified import Data.Set qualified as Set import Galley.API.MLS.CheckClients -import Galley.Effects import Imports import Polysemy import Polysemy.Error @@ -37,8 +36,10 @@ import Wire.API.MLS.CipherSuite import Wire.API.MLS.Credential import Wire.API.MLS.OutOfSync import Wire.API.MLS.SubConversation +import Wire.BrigAPIAccess (BrigAPIAccess) import Wire.ConversationStore import Wire.ConversationStore.MLS.Types +import Wire.FederationAPIAccess (FederationAPIAccess) import Wire.StoredConversation checkConversationOutOfSync :: diff --git a/services/galley/src/Galley/API/MLS/Propagate.hs b/services/galley/src/Galley/API/MLS/Propagate.hs index a8a656a4677..8e71e7463e3 100644 --- a/services/galley/src/Galley/API/MLS/Propagate.hs +++ b/services/galley/src/Galley/API/MLS/Propagate.hs @@ -23,8 +23,6 @@ import Data.Json.Util import Data.List.NonEmpty (NonEmpty, nonEmpty) import Data.Map qualified as Map import Data.Qualified -import Galley.API.Push -import Galley.Effects import Imports import Network.AMQP qualified as Q import Polysemy @@ -42,6 +40,7 @@ import Wire.API.Message import Wire.API.Push.V2 (RecipientClients (..)) import Wire.BackendNotificationQueueAccess import Wire.ConversationStore.MLS.Types +import Wire.ExternalAccess import Wire.NotificationSubsystem import Wire.Sem.Now (Now) import Wire.Sem.Now qualified as Now diff --git a/services/galley/src/Galley/API/MLS/Proposal.hs b/services/galley/src/Galley/API/MLS/Proposal.hs index a66a0f9144e..96985aa75d5 100644 --- a/services/galley/src/Galley/API/MLS/Proposal.hs +++ b/services/galley/src/Galley/API/MLS/Proposal.hs @@ -39,9 +39,6 @@ import Data.Map qualified as Map import Data.Qualified import Data.Set qualified as Set import Galley.API.MLS.IncomingMessage -import Galley.Effects -import Galley.Env -import Galley.Options import Galley.Types.Error import Imports import Polysemy @@ -66,13 +63,19 @@ import Wire.API.MLS.Serialisation import Wire.API.MLS.Validation import Wire.API.MLS.Validation.Error (toText) import Wire.API.Message +import Wire.BackendNotificationQueueAccess import Wire.BrigAPIAccess +import Wire.ConversationStore (ConversationStore) import Wire.ConversationStore.MLS.Types import Wire.ConversationSubsystem.Util +import Wire.ExternalAccess +import Wire.FederationAPIAccess (FederationAPIAccess) +import Wire.LegalHoldStore (LegalHoldStore) import Wire.NotificationSubsystem import Wire.ProposalStore import Wire.Sem.Now (Now) import Wire.TeamCollaboratorsSubsystem +import Wire.TeamStore data ProposalAction = ProposalAction { paAdd :: ClientMap (LeafIndex, Maybe KeyPackage), @@ -129,9 +132,7 @@ type HasProposalEffects r = Member (Error UnreachableBackends) r, Member ExternalAccess r, Member (FederationAPIAccess FederatorClient) r, - Member (Input Env) r, Member (Input (Local ())) r, - Member (Input Opts) r, Member Now r, Member LegalHoldStore r, Member ProposalStore r, diff --git a/services/galley/src/Galley/API/MLS/Removal.hs b/services/galley/src/Galley/API/MLS/Removal.hs index 33a87c2a384..fbdc427bbcf 100644 --- a/services/galley/src/Galley/API/MLS/Removal.hs +++ b/services/galley/src/Galley/API/MLS/Removal.hs @@ -34,7 +34,6 @@ import Data.Set qualified as Set import Galley.API.MLS.Conversation import Galley.API.MLS.Keys import Galley.API.MLS.Propagate -import Galley.Effects import Imports import Polysemy import Polysemy.Error @@ -52,8 +51,10 @@ import Wire.API.MLS.Message import Wire.API.MLS.Proposal import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation +import Wire.BackendNotificationQueueAccess import Wire.ConversationStore import Wire.ConversationStore.MLS.Types +import Wire.ExternalAccess import Wire.NotificationSubsystem import Wire.ProposalStore import Wire.Sem.Now (Now) diff --git a/services/galley/src/Galley/API/MLS/Reset.hs b/services/galley/src/Galley/API/MLS/Reset.hs index 5bd236d4dff..18955e75c1e 100644 --- a/services/galley/src/Galley/API/MLS/Reset.hs +++ b/services/galley/src/Galley/API/MLS/Reset.hs @@ -23,8 +23,6 @@ import Galley.API.Action import Galley.API.MLS.Enabled import Galley.API.MLS.Util import Galley.API.Update -import Galley.Effects -import Galley.Env import Galley.Types.Error import Imports import Polysemy @@ -38,18 +36,23 @@ import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Error +import Wire.API.MLS.Keys (MLSKeysByPurpose, MLSPrivateKeys) import Wire.API.MLS.SubConversation import Wire.API.Routes.Public.Galley.MLS +import Wire.BackendNotificationQueueAccess +import Wire.BrigAPIAccess (BrigAPIAccess) import Wire.ConversationStore import Wire.ConversationSubsystem -import Wire.FederationSubsystem +import Wire.ExternalAccess (ExternalAccess) +import Wire.FederationAPIAccess (FederationAPIAccess) import Wire.NotificationSubsystem +import Wire.ProposalStore (ProposalStore) import Wire.Sem.Now (Now) -import Wire.TeamCollaboratorsSubsystem +import Wire.Sem.Random (Random) import Wire.TeamSubsystem (TeamSubsystem) resetMLSConversation :: - ( Member (Input Env) r, + ( Member (Input (Maybe (MLSKeysByPurpose MLSPrivateKeys))) r, Member Now r, Member (Input (Local ())) r, Member (ErrorS MLSNotEnabled) r, @@ -60,7 +63,6 @@ resetMLSConversation :: Member (Error InternalError) r, Member (ErrorS InvalidOperation) r, Member (ErrorS MLSFederatedResetNotSupported) r, - Member (ErrorS GroupIdVersionNotSupported) r, Member BackendNotificationQueueAccess r, Member ConversationStore r, Member (FederationAPIAccess FederatorClient) r, @@ -73,9 +75,7 @@ resetMLSConversation :: Member Random r, Member Resource r, Member P.TinyLog r, - Member TeamCollaboratorsSubsystem r, Member MLSCommitLockStore r, - Member FederationSubsystem r, Member TeamSubsystem r, Member (Input ConversationSubsystemConfig) r ) => @@ -95,8 +95,7 @@ resetMLSConversation lusr reset = do lusr ( \lcnv -> void $ - updateLocalConversation - @'ConversationResetTag + updateLocalConversationReset lcnv (tUntagged lusr) Nothing @@ -112,7 +111,6 @@ resetRemoteMLSConversation :: Member (ErrorS InvalidOperation) r, Member (ErrorS ConvNotFound) r, Member (ErrorS MLSFederatedResetNotSupported) r, - Member (ErrorS GroupIdVersionNotSupported) r, Member (ErrorS MLSStaleMessage) r, Member (Error FederationError) r, Member (Error InternalError) r, diff --git a/services/galley/src/Galley/API/MLS/SubConversation.hs b/services/galley/src/Galley/API/MLS/SubConversation.hs index 23b88ff07c1..a404476e377 100644 --- a/services/galley/src/Galley/API/MLS/SubConversation.hs +++ b/services/galley/src/Galley/API/MLS/SubConversation.hs @@ -40,8 +40,6 @@ import Galley.API.MLS.Conversation import Galley.API.MLS.GroupInfo import Galley.API.MLS.Removal import Galley.API.MLS.Util -import Galley.App (Env) -import Galley.Effects import Imports import Polysemy import Polysemy.Error @@ -61,14 +59,19 @@ import Wire.API.MLS.Credential import Wire.API.MLS.Group.Serialisation import Wire.API.MLS.Group.Serialisation qualified as Group import Wire.API.MLS.GroupInfo +import Wire.API.MLS.Keys (MLSKeysByPurpose, MLSPrivateKeys) 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.BackendNotificationQueueAccess +import Wire.ConversationStore qualified as Conversation +import Wire.ConversationStore.MLS.Types as Conversation import Wire.ConversationSubsystem.Util +import Wire.ExternalAccess (ExternalAccess) import Wire.FederationAPIAccess import Wire.NotificationSubsystem +import Wire.ProposalStore (ProposalStore) import Wire.Sem.Now (Now) +import Wire.Sem.Random (Random) import Wire.StoredConversation import Wire.StoredConversation qualified as Data import Wire.TeamSubsystem (TeamSubsystem) @@ -80,7 +83,7 @@ type MLSGetSubConvStaticErrors = ] getSubConversation :: - ( Member ConversationStore r, + ( Member Conversation.ConversationStore r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'MLSSubConvUnsupportedConvType) r, @@ -100,7 +103,7 @@ getSubConversation lusr qconv sconv = do qconv getLocalSubConversation :: - ( Member ConversationStore r, + ( Member Conversation.ConversationStore r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'MLSSubConvUnsupportedConvType) r, @@ -116,7 +119,7 @@ getLocalSubConversation qusr lconv sconv = do unless (Data.convType c == RegularConv || Data.convType c == One2OneConv) $ throwS @'MLSSubConvUnsupportedConvType - msub <- Eff.getSubConversation (tUnqualified lconv) sconv + msub <- Conversation.getSubConversation (tUnqualified lconv) sconv sub <- case msub of Nothing -> do (_mlsMeta, mlsProtocol) <- noteS @'ConvNotFound (mlsMetadata c) @@ -163,10 +166,10 @@ getRemoteSubConversation lusr rcnv sconv = do getSubConversationGroupInfo :: ( Members - '[ ConversationStore, + '[ Conversation.ConversationStore, Error FederationError, FederationAPIAccess FederatorClient, - Input Env + Input (Maybe (MLSKeysByPurpose MLSPrivateKeys)) ] r, Members MLSGroupInfoStaticErrors r @@ -184,7 +187,7 @@ getSubConversationGroupInfo lusr qcnvId subconv = do qcnvId getSubConversationGroupInfoFromLocalConv :: - (Member ConversationStore r) => + (Member Conversation.ConversationStore r) => (Members MLSGroupInfoStaticErrors r) => Qualified UserId -> SubConvId -> @@ -192,7 +195,7 @@ getSubConversationGroupInfoFromLocalConv :: Sem r GroupInfoData getSubConversationGroupInfoFromLocalConv qusr subConvId lcnvId = do void $ getLocalConvForUser qusr lcnvId - Eff.getSubConversationGroupInfo (tUnqualified lcnvId) subConvId + Conversation.getSubConversationGroupInfo (tUnqualified lcnvId) subConvId >>= noteS @'MLSMissingGroupInfo type MLSDeleteSubConvStaticErrors = @@ -203,16 +206,16 @@ type MLSDeleteSubConvStaticErrors = ] deleteSubConversation :: - ( Member ConversationStore r, + ( Member Conversation.ConversationStore r, Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'MLSNotEnabled) r, Member (ErrorS 'MLSStaleMessage) r, Member (Error FederationError) r, Member (FederationAPIAccess FederatorClient) r, - Member (Input Env) r, + Member (Input (Maybe (MLSKeysByPurpose MLSPrivateKeys))) r, Member Resource r, - Member Eff.MLSCommitLockStore r, + Member Conversation.MLSCommitLockStore r, Member TeamSubsystem r ) => Local UserId -> @@ -260,11 +263,11 @@ resetRemoteSubConversation lusr rcnvId scnvId reset = do type HasLeaveSubConversationEffects r = ( Member BackendNotificationQueueAccess r, - Member ConversationStore r, + Member Conversation.ConversationStore r, Member ExternalAccess r, Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, - Member (Input Env) r, + Member (Input (Maybe (MLSKeysByPurpose MLSPrivateKeys))) r, Member Now r, Member ProposalStore r, Member Random r, @@ -286,7 +289,7 @@ leaveSubConversation :: Member (ErrorS 'MLSNotEnabled) r, Member Resource r, Members LeaveSubConversationStaticErrors r, - Member Eff.MLSCommitLockStore r, + Member Conversation.MLSCommitLockStore r, Member TeamSubsystem r, Member (Input ConversationSubsystemConfig) r ) => @@ -313,7 +316,7 @@ leaveLocalSubConversation :: Member (Error FederationError) r, Member Resource r, Members LeaveSubConversationStaticErrors r, - Member Eff.MLSCommitLockStore r, + Member Conversation.MLSCommitLockStore r, Member TeamSubsystem r, Member (Input ConversationSubsystemConfig) r ) => @@ -327,13 +330,13 @@ leaveLocalSubConversation cid lcnv sub = do mlsConv <- noteS @'ConvNotFound =<< mkMLSConversation cnv subConv <- noteS @'ConvNotFound - =<< Eff.getSubConversation (tUnqualified lcnv) sub + =<< Conversation.getSubConversation (tUnqualified lcnv) sub idx <- note (mlsProtocolError "Client is not a member of the subconversation") $ cmLookupIndex cid (scMembers subConv) let (gid, epoch) = (cnvmlsGroupId &&& cnvmlsEpoch) (scMLSData subConv) -- plan to remove the leaver from the member list - Eff.planClientRemoval gid (Identity cid) + Conversation.planClientRemoval gid (Identity cid) let cm = cmRemoveClient cid (scMembers subConv) if cmNull cm then do @@ -382,12 +385,12 @@ leaveRemoteSubConversation cid rcnv sub = do LeaveSubConversationResponseOk -> pure () resetLocalSubConversation :: - ( Member ConversationStore r, + ( Member Conversation.ConversationStore r, Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'MLSStaleMessage) r, Member Resource r, - Member Eff.MLSCommitLockStore r, + Member Conversation.MLSCommitLockStore r, Member TeamSubsystem r ) => Qualified UserId -> @@ -403,11 +406,11 @@ resetLocalSubConversation qusr lcnvId scnvId reset = do lowerCodensity $ do withCommitLock lConvOrSubId reset.groupId reset.epoch lift $ do - sconv <- Eff.getSubConversation cnvId scnvId >>= noteS @'ConvNotFound + sconv <- Conversation.getSubConversation cnvId scnvId >>= noteS @'ConvNotFound let (gid, epoch) = (cnvmlsGroupId &&& cnvmlsEpoch) (scMLSData sconv) unless (reset.groupId == gid) $ throwS @'ConvNotFound unless (reset.epoch == epoch) $ throwS @'MLSStaleMessage - Eff.removeAllMLSClients gid + Conversation.removeAllMLSClients gid -- swallowing the error and starting with GroupIdGen 0 if nextGenGroupId fails let newGid = @@ -419,4 +422,4 @@ resetLocalSubConversation qusr lcnvId scnvId reset = do $ nextGenGroupId gid -- the following overwrites any prior information about the subconversation - void $ Eff.upsertSubConversation cnvId scnvId newGid + void $ Conversation.upsertSubConversation cnvId scnvId newGid diff --git a/services/galley/src/Galley/API/MLS/Util.hs b/services/galley/src/Galley/API/MLS/Util.hs index 8fed49aa54c..742f820b1bb 100644 --- a/services/galley/src/Galley/API/MLS/Util.hs +++ b/services/galley/src/Galley/API/MLS/Util.hs @@ -24,7 +24,6 @@ import Data.Id import Data.Qualified import Data.Set qualified as Set import Data.Text qualified as T -import Galley.Effects import Imports import Polysemy import Polysemy.Error @@ -133,5 +132,5 @@ getConvFromGroupId :: GroupId -> Sem r (ConvType, Qualified ConvOrSubConvId) getConvFromGroupId gid = case groupIdToConv gid of - Left e -> throw (mlsProtocolError (T.pack e)) + Left e -> throw (mlsProtocolError ("Could not parse group ID: " <> T.pack e)) Right (_, parts) -> pure (parts.convType, parts.qConvId) diff --git a/services/galley/src/Galley/API/MLS/Welcome.hs b/services/galley/src/Galley/API/MLS/Welcome.hs index 115ad8faab1..44484dda0a5 100644 --- a/services/galley/src/Galley/API/MLS/Welcome.hs +++ b/services/galley/src/Galley/API/MLS/Welcome.hs @@ -29,7 +29,6 @@ import Data.Json.Util import Data.Map qualified as Map import Data.Qualified import Data.Time -import Galley.API.Push import Imports import Network.Wai.Utilities.JSONResponse import Polysemy diff --git a/services/galley/src/Galley/API/Meetings.hs b/services/galley/src/Galley/API/Meetings.hs index bd7acf1a33e..3321e85ae04 100644 --- a/services/galley/src/Galley/API/Meetings.hs +++ b/services/galley/src/Galley/API/Meetings.hs @@ -19,6 +19,9 @@ module Galley.API.Meetings ( createMeeting, updateMeeting, getMeeting, + listMeetings, + addMeetingInvitation, + removeMeetingInvitation, ) where @@ -104,3 +107,51 @@ getMeeting zUser domain meetingId = do case maybeMeeting of Nothing -> throwS @'MeetingNotFound Just meeting -> pure meeting + +listMeetings :: + ( Member Meetings.MeetingsSubsystem r, + Member TeamStore.TeamStore r, + Member FeaturesConfigSubsystem r, + Member (ErrorS 'InvalidOperation) r + ) => + Local UserId -> + Sem r [Meeting] +listMeetings lUser = do + checkMeetingsEnabled (tUnqualified lUser) + Meetings.listMeetings lUser + +addMeetingInvitation :: + ( Member Meetings.MeetingsSubsystem r, + Member (ErrorS 'MeetingNotFound) r, + Member TeamStore.TeamStore r, + Member FeaturesConfigSubsystem r, + Member (ErrorS 'InvalidOperation) r + ) => + Local UserId -> + Domain -> + MeetingId -> + MeetingEmailsInvitation -> + Sem r () +addMeetingInvitation zUser domain meetingId (MeetingEmailsInvitation emails) = do + checkMeetingsEnabled (tUnqualified zUser) + let qMeetingId = Qualified meetingId domain + success <- Meetings.addInvitedEmails zUser qMeetingId emails + unless success $ throwS @'MeetingNotFound + +removeMeetingInvitation :: + ( Member Meetings.MeetingsSubsystem r, + Member (ErrorS 'MeetingNotFound) r, + Member TeamStore.TeamStore r, + Member FeaturesConfigSubsystem r, + Member (ErrorS 'InvalidOperation) r + ) => + Local UserId -> + Domain -> + MeetingId -> + MeetingEmailsInvitation -> + Sem r () +removeMeetingInvitation zUser domain meetingId (MeetingEmailsInvitation emails) = do + checkMeetingsEnabled (tUnqualified zUser) + let qMeetingId = Qualified meetingId domain + success <- Meetings.removeInvitedEmails zUser qMeetingId emails + unless success $ throwS @'MeetingNotFound diff --git a/services/galley/src/Galley/API/Message.hs b/services/galley/src/Galley/API/Message.hs index 1c033d83ac9..502f2d53aa9 100644 --- a/services/galley/src/Galley/API/Message.hs +++ b/services/galley/src/Galley/API/Message.hs @@ -23,7 +23,6 @@ module Galley.API.Message postRemoteOtrMessage, legacyClientMismatchStrategy, Unqualify (..), - userToProtectee, MessageMetadata (..), -- * Only exported for tests @@ -50,8 +49,6 @@ import Data.Set qualified as Set import Data.Set.Lens import Data.Time.Clock (UTCTime) import Galley.API.LegalHold.Conflicts -import Galley.API.Push -import Galley.Effects import Galley.Options import Galley.Types.Clients qualified as Clients import Imports hiding (forkIO) @@ -82,8 +79,9 @@ import Wire.BrigAPIAccess import Wire.ConversationStore import Wire.ConversationSubsystem qualified as ConvSubsystem import Wire.ConversationSubsystem.Util +import Wire.ExternalAccess import Wire.FederationAPIAccess -import Wire.NotificationSubsystem (NotificationSubsystem) +import Wire.NotificationSubsystem (BotMap, NotificationSubsystem, newMessagePush, runMessagePush) import Wire.Sem.Now (Now) import Wire.Sem.Now qualified as Now import Wire.StoredConversation diff --git a/services/galley/src/Galley/API/Public/Bot.hs b/services/galley/src/Galley/API/Public/Bot.hs index 1a28c98dfe3..ecb4f18eb38 100644 --- a/services/galley/src/Galley/API/Public/Bot.hs +++ b/services/galley/src/Galley/API/Public/Bot.hs @@ -23,8 +23,6 @@ import Galley.API.Query qualified as Query import Galley.API.Teams.Features qualified as Features import Galley.API.Update import Galley.App -import Galley.Effects -import Galley.Effects qualified as E import Polysemy import Polysemy.Input import Wire.API.Error @@ -33,7 +31,9 @@ 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.ConversationStore (ConversationStore) import Wire.FeaturesConfigSubsystem (FeaturesConfigSubsystem) +import Wire.TeamStore (TeamStore) import Wire.TeamSubsystem (TeamSubsystem) botAPI :: API BotAPI GalleyEffects @@ -43,7 +43,7 @@ botAPI = getBotConversation :: forall r. - ( Member E.ConversationStore r, + ( Member ConversationStore r, Member (Input (Local ())) r, Member (ErrorS 'AccessDenied) r, Member (ErrorS 'ConvNotFound) r, diff --git a/services/galley/src/Galley/API/Public/Feature.hs b/services/galley/src/Galley/API/Public/Feature.hs index 81f8d6247c8..0c55a19471b 100644 --- a/services/galley/src/Galley/API/Public/Feature.hs +++ b/services/galley/src/Galley/API/Public/Feature.hs @@ -43,7 +43,7 @@ featureAPI = <@> featureAPIGetPut <@> mkNamedAPI @"get-search-visibility" getSearchVisibility <@> mkNamedAPI @"set-search-visibility" (setSearchVisibility (featureEnabledForTeam @SearchVisibilityAvailableConfig)) - <@> mkNamedAPI @'("get", ValidateSAMLEmailsConfig) getFeature + <@> mkNamedAPI @'("get", RequireExternalEmailVerificationConfig) getFeature <@> mkNamedAPI @'("get", DigitalSignaturesConfig) getFeature <@> featureAPIGetPut <@> featureAPIGetPut @@ -86,7 +86,7 @@ deprecatedFeatureConfigAPI :: API DeprecatedFeatureAPI GalleyEffects deprecatedFeatureConfigAPI = mkNamedAPI @'("get-deprecated", '(SearchVisibilityAvailableConfig, V2)) getFeature <@> mkNamedAPI @'("put-deprecated", '(SearchVisibilityAvailableConfig, V2)) setFeature - <@> mkNamedAPI @'("get-deprecated", '(ValidateSAMLEmailsConfig, V2)) getFeature + <@> mkNamedAPI @'("get-deprecated", '(RequireExternalEmailVerificationConfig, V2)) getFeature <@> mkNamedAPI @'("get-deprecated", '(DigitalSignaturesConfig, V2)) getFeature deprecatedFeatureAPI :: API (AllDeprecatedFeatureConfigAPI DeprecatedFeatureConfigs) GalleyEffects @@ -94,7 +94,7 @@ deprecatedFeatureAPI = mkNamedAPI @'("get-config", LegalholdConfig) getSingleFeatureForUser <@> mkNamedAPI @'("get-config", SSOConfig) getSingleFeatureForUser <@> mkNamedAPI @'("get-config", SearchVisibilityAvailableConfig) getSingleFeatureForUser - <@> mkNamedAPI @'("get-config", ValidateSAMLEmailsConfig) getSingleFeatureForUser + <@> mkNamedAPI @'("get-config", RequireExternalEmailVerificationConfig) getSingleFeatureForUser <@> mkNamedAPI @'("get-config", DigitalSignaturesConfig) getSingleFeatureForUser <@> mkNamedAPI @'("get-config", AppLockConfig) getSingleFeatureForUser <@> mkNamedAPI @'("get-config", FileSharingConfig) getSingleFeatureForUser diff --git a/services/galley/src/Galley/API/Public/Meetings.hs b/services/galley/src/Galley/API/Public/Meetings.hs index 838ff3d1233..e494c5d1f65 100644 --- a/services/galley/src/Galley/API/Public/Meetings.hs +++ b/services/galley/src/Galley/API/Public/Meetings.hs @@ -27,3 +27,6 @@ meetingsAPI = mkNamedAPI @"create-meeting" Meetings.createMeeting <@> mkNamedAPI @"update-meeting" Meetings.updateMeeting <@> mkNamedAPI @"get-meeting" Meetings.getMeeting + <@> mkNamedAPI @"list-meetings" Meetings.listMeetings + <@> mkNamedAPI @"add-meeting-invitation" Meetings.addMeetingInvitation + <@> mkNamedAPI @"remove-meeting-invitation" Meetings.removeMeetingInvitation diff --git a/services/galley/src/Galley/API/Public/TeamNotification.hs b/services/galley/src/Galley/API/Public/TeamNotification.hs index 03b999ade21..770db1734f3 100644 --- a/services/galley/src/Galley/API/Public/TeamNotification.hs +++ b/services/galley/src/Galley/API/Public/TeamNotification.hs @@ -22,7 +22,6 @@ import Data.Range import Data.UUID.Util qualified as UUID import Galley.API.Teams.Notifications qualified as APITeamQueue import Galley.App -import Galley.Effects import Imports import Polysemy import Wire.API.Error @@ -30,6 +29,8 @@ import Wire.API.Error.Galley import Wire.API.Internal.Notification import Wire.API.Routes.API import Wire.API.Routes.Public.Galley.TeamNotification +import Wire.BrigAPIAccess (BrigAPIAccess) +import Wire.TeamNotificationStore (TeamNotificationStore) teamNotificationAPI :: API TeamNotificationAPI GalleyEffects teamNotificationAPI = diff --git a/services/galley/src/Galley/API/Push.hs b/services/galley/src/Galley/API/Push.hs deleted file mode 100644 index 404506590d9..00000000000 --- a/services/galley/src/Galley/API/Push.hs +++ /dev/null @@ -1,105 +0,0 @@ -{-# LANGUAGE TupleSections #-} -{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} - --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Galley.API.Push - ( -- * Message pushes - MessagePush (..), - - -- * Executing message pushes - BotMap, - newMessagePush, - runMessagePush, - ) -where - -import Data.Default -import Data.Id -import Data.Json.Util -import Data.List.NonEmpty qualified as NonEmpty -import Data.Map qualified as Map -import Data.Qualified -import Imports -import Polysemy -import Polysemy.TinyLog -import System.Logger.Class qualified as Log -import Wire.API.Event.Conversation -import Wire.API.Message -import Wire.API.Push.V2 (RecipientClients (RecipientClientsSome), Route (..)) -import Wire.ExternalAccess -import Wire.NotificationSubsystem -import Wire.StoredConversation - -data MessagePush - = MessagePush (Maybe ConnId) MessageMetadata [Recipient] [BotMember] Event - -type BotMap = Map UserId BotMember - -class ToRecipient a where - toRecipient :: a -> Recipient - -instance ToRecipient (UserId, ClientId) where - toRecipient (u, c) = Recipient u (RecipientClientsSome (NonEmpty.singleton c)) - -instance ToRecipient Recipient where - toRecipient = id - -newMessagePush :: - (ToRecipient r) => - BotMap -> - Maybe ConnId -> - MessageMetadata -> - [r] -> - Event -> - MessagePush -newMessagePush botMap mconn mm userOrBots event = - let toPair r = case Map.lookup (recipientUserId r) botMap of - Just botMember -> ([], [botMember]) - Nothing -> ([r], []) - (recipients, botMembers) = foldMap (toPair . toRecipient) userOrBots - in MessagePush mconn mm recipients botMembers event - -runMessagePush :: - forall x r. - ( Member ExternalAccess r, - Member TinyLog r, - Member NotificationSubsystem r - ) => - Local x -> - Maybe (Qualified ConvId) -> - MessagePush -> - Sem r () -runMessagePush loc mqcnv mp@(MessagePush _ _ _ botMembers event) = do - pushNotifications [toPush mp] - for_ mqcnv $ \qcnv -> - if tDomain loc /= qDomain qcnv - then unless (null botMembers) $ do - warn $ Log.msg ("Ignoring messages for local bots in a remote conversation" :: ByteString) . Log.field "conversation" (show qcnv) - else deliverAndDeleteAsync (qUnqualified qcnv) (map (,event) botMembers) - -toPush :: MessagePush -> Push -toPush (MessagePush mconn mm rs _ event) = - def - { origin = Just (qUnqualified (eventFromUserId (evtFrom event))), - conn = mconn, - json = toJSONObject event, - recipients = rs, - route = bool RouteDirect RouteAny (mmNativePush mm), - transient = mmTransient mm - } diff --git a/services/galley/src/Galley/API/Query.hs b/services/galley/src/Galley/API/Query.hs index 6904de09b2b..777d7ab7ec7 100644 --- a/services/galley/src/Galley/API/Query.hs +++ b/services/galley/src/Galley/API/Query.hs @@ -72,8 +72,6 @@ import Galley.API.MLS.One2One import Galley.API.Mapping import Galley.API.Mapping qualified as Mapping import Galley.API.Teams.Features.Get -import Galley.Effects -import Galley.Env import Galley.Types.Error import Imports import Polysemy @@ -103,11 +101,13 @@ import Wire.API.Routes.MultiTablePaging qualified as Public import Wire.API.Team.Feature as Public import Wire.API.Team.Member (HiddenPerm (..), TeamMember) import Wire.API.User +import Wire.BrigAPIAccess (BrigAPIAccess) import Wire.CodeStore import Wire.CodeStore.Code (Code (codeConversation)) import Wire.CodeStore.Code qualified as Data -import Wire.ConversationStore qualified as E +import Wire.ConversationStore qualified as ConversationStore import Wire.ConversationStore.MLS.Types +import Wire.ConversationSubsystem qualified as ConversationSubsystem import Wire.ConversationSubsystem.One2One import Wire.ConversationSubsystem.Util import Wire.FeaturesConfigSubsystem @@ -118,12 +118,13 @@ 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 import Wire.UserList getBotConversation :: - ( Member ConversationStore r, + ( Member ConversationStore.ConversationStore r, Member (ErrorS 'ConvNotFound) r, Member (Input (Local ())) r, Member TeamSubsystem r @@ -148,7 +149,7 @@ getBotConversation zbot cnv = do getUnqualifiedOwnConversation :: forall r. - ( Member ConversationStore r, + ( Member ConversationStore.ConversationStore r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvAccessDenied) r, Member (Error InternalError) r, @@ -164,7 +165,7 @@ getUnqualifiedOwnConversation lusr cnv = do getUnqualifiedConversation :: forall r. - ( Member ConversationStore r, + ( Member ConversationStore.ConversationStore r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvAccessDenied) r, Member TeamSubsystem r @@ -178,11 +179,11 @@ getUnqualifiedConversation lusr cnv = getConversation :: forall r. - ( Member ConversationStore r, + ( Member ConversationStore.ConversationStore r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvAccessDenied) r, Member (Error FederationError) r, - Member (FederationAPIAccess FederatorClient) r, + Member (E.FederationAPIAccess FederatorClient) r, Member P.TinyLog r, Member TeamSubsystem r ) => @@ -198,12 +199,12 @@ getConversation lusr cnv = getOwnConversation :: forall r. - ( Member ConversationStore r, + ( Member ConversationStore.ConversationStore r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvAccessDenied) r, Member (Error FederationError) r, Member (Error InternalError) r, - Member (FederationAPIAccess FederatorClient) r, + Member (E.FederationAPIAccess FederatorClient) r, Member P.TinyLog r, Member TeamSubsystem r ) => @@ -218,11 +219,11 @@ getOwnConversation lusr cnv = do cnv getRemoteConversation :: - ( Member ConversationStore r, + ( Member ConversationStore.ConversationStore r, Member (ErrorS ConvNotFound) r, Member (Error FederationError) r, Member TinyLog r, - Member (FederationAPIAccess FederatorClient) r + Member (E.FederationAPIAccess FederatorClient) r ) => Local UserId -> Remote ConvId -> @@ -235,10 +236,10 @@ getRemoteConversation lusr remoteConvId = do _convs -> throw $ FederationUnexpectedBody "expected one conversation, got multiple" getRemoteConversations :: - ( Member ConversationStore r, + ( Member ConversationStore.ConversationStore r, Member (Error FederationError) r, Member (ErrorS 'ConvNotFound) r, - Member (FederationAPIAccess FederatorClient) r, + Member (E.FederationAPIAccess FederatorClient) r, Member P.TinyLog r ) => Local UserId -> @@ -253,7 +254,7 @@ getRemoteConversations lusr remoteConvs = getLocalConversationInternal :: ( Member (Input (Local ())) r, Member (ErrorS ConvNotFound) r, - Member ConversationStore r + Member ConversationStore.ConversationStore r ) => ConvId -> Sem r Conversation @@ -306,8 +307,8 @@ partitionGetConversationFailures = bimap concat concat . partitionEithers . map split (FailedGetConversation convs (FailedGetConversationRemotely _)) = Right convs getRemoteConversationsWithFailures :: - ( Member ConversationStore r, - Member (FederationAPIAccess FederatorClient) r, + ( Member ConversationStore.ConversationStore r, + Member (E.FederationAPIAccess FederatorClient) r, Member P.TinyLog r ) => Local UserId -> @@ -315,7 +316,7 @@ getRemoteConversationsWithFailures :: Sem r ([FailedGetConversation], [Public.OwnConversation]) getRemoteConversationsWithFailures lusr convs = do -- get self member statuses from the database - statusMap <- E.getRemoteConversationStatus (tUnqualified lusr) convs + statusMap <- ConversationStore.getRemoteConversationStatus (tUnqualified lusr) convs let remoteView :: Remote RemoteConversationV2 -> OwnConversation remoteView rconv = Mapping.remoteConversationView @@ -360,7 +361,7 @@ getRemoteConversationsWithFailures lusr convs = do handleFailure (Right c) = pure . Right . traverse (.convs) $ c getConversationRoles :: - ( Member ConversationStore r, + ( Member ConversationStore.ConversationStore r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvAccessDenied) r, Member TeamSubsystem r @@ -375,14 +376,14 @@ getConversationRoles lusr cnv = do pure $ Public.ConversationRolesList wireConvRoles conversationIdsPageFromUnqualified :: - (Member ConversationStore r) => + (Member ConversationStore.ConversationStore r) => Local UserId -> Maybe ConvId -> Maybe (Range 1 1000 Int32) -> Sem r (Public.ConversationList ConvId) conversationIdsPageFromUnqualified lusr start msize = do let size = fromMaybe (toRange (Proxy @1000)) msize - ids <- E.getLocalConversationIds (tUnqualified lusr) start size + ids <- ConversationStore.getLocalConversationIds (tUnqualified lusr) start size pure $ Public.ConversationList (resultSetResult ids) @@ -400,13 +401,13 @@ conversationIdsPageFromUnqualified lusr start msize = do -- FUTUREWORK: Move the body of this function to 'conversationIdsPageFrom' once -- support for V2 is dropped. conversationIdsPageFromV2 :: - (Member ConversationStore r) => + (Member ConversationSubsystem.ConversationSubsystem r) => ListGlobalSelfConvs -> Local UserId -> Public.GetPaginatedConversationIds -> Sem r Public.ConvIdsPage conversationIdsPageFromV2 listGlobalSelf lusr Public.GetMultiTablePageRequest {..} = do - filterOut <$> E.getConversationIds lusr gmtprSize gmtprState + filterOut <$> ConversationSubsystem.getConversationIds lusr gmtprSize gmtprState where -- MLS self-conversation of this user selfConvId = mlsSelfConvId (tUnqualified lusr) @@ -434,9 +435,10 @@ conversationIdsPageFromV2 listGlobalSelf lusr Public.GetMultiTablePageRequest {. -- - lexicographically by their domain and then by their id. conversationIdsPageFrom :: forall r. - ( Member ConversationStore r, + ( Member ConversationStore.ConversationStore r, + Member ConversationSubsystem.ConversationSubsystem r, Member (Error InternalError) r, - Member (Input Env) r, + Member (Input (Maybe (MLSKeysByPurpose MLSPrivateKeys))) r, Member P.TinyLog r ) => Local UserId -> @@ -456,7 +458,7 @@ conversationIdsPageFrom lusr state = do getConversations :: ( Member (Error InternalError) r, - Member ConversationStore r, + Member ConversationStore.ConversationStore r, Member P.TinyLog r ) => Local UserId -> @@ -469,7 +471,7 @@ getConversations luser mids mstart msize = do flip ConversationList more <$> mapM (Mapping.conversationViewV9 luser) cs getConversationsInternal :: - (Member ConversationStore r) => + (Member ConversationStore.ConversationStore r) => Local UserId -> Maybe (Range 1 32 (CommaSeparatedList ConvId)) -> Maybe ConvId -> @@ -479,7 +481,7 @@ getConversationsInternal luser mids mstart msize = do (more, ids) <- getIds mids let localConvIds = ids cs <- - E.getConversations localConvIds + ConversationStore.getConversations localConvIds >>= filterM (\c -> pure $ isMember (tUnqualified luser) c.localMembers) pure $ Public.ConversationList cs more where @@ -487,23 +489,23 @@ getConversationsInternal luser mids mstart msize = do -- get ids and has_more flag getIds :: - (Member ConversationStore r) => + (Member ConversationStore.ConversationStore r) => Maybe (Range 1 32 (CommaSeparatedList ConvId)) -> Sem r (Bool, [ConvId]) getIds (Just ids) = (False,) - <$> E.selectConversations + <$> ConversationStore.selectConversations (tUnqualified luser) (fromCommaSeparatedList (fromRange ids)) getIds Nothing = do - r <- E.getLocalConversationIds (tUnqualified luser) mstart (rcast size) + r <- ConversationStore.getLocalConversationIds (tUnqualified luser) mstart (rcast size) let hasMore = resultSetType r == ResultSetTruncated pure (hasMore, resultSetResult r) listConversations :: - ( Member ConversationStore r, + ( Member ConversationStore.ConversationStore r, Member (Error InternalError) r, - Member (FederationAPIAccess FederatorClient) r, + Member (E.FederationAPIAccess FederatorClient) r, Member P.TinyLog r ) => Local UserId -> @@ -512,10 +514,10 @@ listConversations :: listConversations luser (Public.ListConversations ids) = do let (localIds, remoteIds) = partitionQualified luser (fromRange ids) (foundLocalIds, notFoundLocalIds) <- - foundsAndNotFounds (E.selectConversations (tUnqualified luser)) localIds + foundsAndNotFounds (ConversationStore.selectConversations (tUnqualified luser)) localIds localInternalConversations <- - E.getConversations foundLocalIds + ConversationStore.getConversations foundLocalIds >>= filterM (\c -> pure $ isMember (tUnqualified luser) c.localMembers) localConversations <- mapM (Mapping.conversationViewV9 luser) localInternalConversations @@ -550,7 +552,7 @@ listConversations luser (Public.ListConversations ids) = do pure (founds, notFounds) iterateConversations :: - (Member ConversationStore r) => + (Member ConversationStore.ConversationStore r) => Local UserId -> Range 1 500 Int32 -> ([StoredConversation] -> Sem r a) -> @@ -569,7 +571,7 @@ iterateConversations luid pageSize handleConvs = go Nothing pure $ resultHead : resultTail internalGetMember :: - ( Member ConversationStore r, + ( Member ConversationStore.ConversationStore r, Member (Error FederationError) r, Member (Input (Local ())) r ) => @@ -583,11 +585,11 @@ internalGetMember qcnv usr = do getSelfMember :: forall r. - ( Member ConversationStore r, + ( Member ConversationStore.ConversationStore r, Member (ErrorS ConvNotFound) r, Member (Error FederationError) r, Member TinyLog r, - Member (FederationAPIAccess FederatorClient) r + Member (E.FederationAPIAccess FederatorClient) r ) => Local UserId -> Qualified ConvId -> @@ -607,34 +609,34 @@ getSelfMember lusr cnv = do pure $ Just $ conv.cnvMembers.cmSelf getLocalSelf :: - (Member ConversationStore r) => + (Member ConversationStore.ConversationStore r) => Local UserId -> ConvId -> Sem r (Maybe Public.Member) getLocalSelf lusr cnv = do do - alive <- E.isConversationAlive cnv + alive <- ConversationStore.isConversationAlive cnv if alive - then localMemberToPublic lusr <$$> E.getLocalMember cnv (tUnqualified lusr) - else Nothing <$ E.deleteConversation cnv + then localMemberToPublic lusr <$$> ConversationStore.getLocalMember cnv (tUnqualified lusr) + else Nothing <$ ConversationStore.deleteConversation cnv getConversationMeta :: - ( Member ConversationStore r, + ( Member ConversationStore.ConversationStore r, Member (ErrorS 'ConvNotFound) r ) => ConvId -> Sem r ConversationMetadata getConversationMeta cnv = ifM - (E.isConversationAlive cnv) - (E.getConversationMetadata cnv >>= noteS @'ConvNotFound) - (E.deleteConversation cnv >> throwS @'ConvNotFound) + (ConversationStore.isConversationAlive cnv) + (ConversationStore.getConversationMetadata cnv >>= noteS @'ConvNotFound) + (ConversationStore.deleteConversation cnv >> throwS @'ConvNotFound) getConversationByReusableCode :: forall r. ( Member BrigAPIAccess r, Member CodeStore r, - Member ConversationStore r, + Member ConversationStore.ConversationStore r, Member (ErrorS 'CodeNotFound) r, Member (ErrorS 'InvalidConversationPassword) r, Member (ErrorS 'ConvNotFound) r, @@ -652,7 +654,7 @@ getConversationByReusableCode :: Sem r ConversationCoverView getConversationByReusableCode lusr key value = do c <- verifyReusableCode (RateLimitUser (tUnqualified lusr)) False Nothing (ConversationCode key value) - conv <- E.getConversation (codeConversation c) >>= noteS @'ConvNotFound + conv <- ConversationStore.getConversation (codeConversation c) >>= noteS @'ConvNotFound ensureConversationAccess (tUnqualified lusr) conv CodeAccess ensureGuestLinksEnabled (Data.convTeam conv) pure $ coverView c conv @@ -679,7 +681,7 @@ ensureGuestLinksEnabled mbTid = getConversationGuestLinksStatus :: forall r. - ( Member ConversationStore r, + ( Member ConversationStore.ConversationStore r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvAccessDenied) r, Member FeaturesConfigSubsystem r, @@ -689,7 +691,7 @@ getConversationGuestLinksStatus :: ConvId -> Sem r (LockableFeature GuestLinksConfig) getConversationGuestLinksStatus uid convId = do - conv <- E.getConversation convId >>= noteS @'ConvNotFound + conv <- ConversationStore.getConversation convId >>= noteS @'ConvNotFound mTeamMember <- maybe (pure Nothing) (TeamSubsystem.internalGetTeamMember uid) conv.metadata.cnvmTeam ensureConvAdmin conv uid mTeamMember getConversationGuestLinksFeatureStatus (Data.convTeam conv) @@ -707,10 +709,10 @@ getConversationGuestLinksFeatureStatus (Just tid) = getFeatureForTeam tid -- the backend removal key). getMLSSelfConversationWithError :: forall r. - ( Member ConversationStore r, + ( Member ConversationStore.ConversationStore r, Member (Error InternalError) r, Member (ErrorS 'MLSNotEnabled) r, - Member (Input Env) r, + Member (Input (Maybe (MLSKeysByPurpose MLSPrivateKeys))) r, Member P.TinyLog r ) => Local UserId -> @@ -727,7 +729,7 @@ getMLSSelfConversationWithError lusr = do -- number. getMLSSelfConversation :: forall r. - ( Member ConversationStore r, + ( Member ConversationStore.ConversationStore r, Member (Error InternalError) r, Member P.TinyLog r ) => @@ -735,12 +737,12 @@ getMLSSelfConversation :: Sem r OwnConversation getMLSSelfConversation lusr = do let selfConvId = mlsSelfConvId . tUnqualified $ lusr - mconv <- E.getConversation selfConvId + mconv <- ConversationStore.getConversation selfConvId cnv <- maybe (createMLSSelfConversation lusr) pure mconv conversationViewV9 lusr cnv createMLSSelfConversation :: - (Member ConversationStore r) => + (Member ConversationStore.ConversationStore r) => Local UserId -> Sem r StoredConversation createMLSSelfConversation lusr = do @@ -754,7 +756,7 @@ createMLSSelfConversation lusr = do protocol = BaseProtocolMLSTag, groupId = Nothing } - E.upsertConversation lcnv nc + ConversationStore.upsertConversation lcnv nc -- | Get an MLS 1-1 conversation. If not already existing, the conversation -- object is created on the fly, but not persisted. The conversation will only @@ -766,14 +768,14 @@ createMLSSelfConversation lusr = do -- two is responsible for hosting the conversation. getMLSOne2OneConversationV5 :: ( Member BrigAPIAccess r, - Member ConversationStore r, - Member (Input Env) r, + Member ConversationStore.ConversationStore r, + Member (Input (Maybe (MLSKeysByPurpose MLSPrivateKeys))) r, Member (Error FederationError) r, Member (Error InternalError) r, Member (ErrorS 'MLSNotEnabled) r, Member (ErrorS 'NotConnected) r, Member (ErrorS 'MLSFederatedOne2OneNotSupported) r, - Member (FederationAPIAccess FederatorClient) r, + Member (E.FederationAPIAccess FederatorClient) r, Member TeamStore r, Member P.TinyLog r, Member TeamCollaboratorsSubsystem r, @@ -790,13 +792,13 @@ getMLSOne2OneConversationV5 lself qother = do getMLSOne2OneConversationInternal :: forall r. ( Member BrigAPIAccess r, - Member ConversationStore r, - Member (Input Env) r, + Member ConversationStore.ConversationStore r, + Member (Input (Maybe (MLSKeysByPurpose MLSPrivateKeys))) r, Member (Error FederationError) r, Member (Error InternalError) r, Member (ErrorS 'MLSNotEnabled) r, Member (ErrorS 'NotConnected) r, - Member (FederationAPIAccess FederatorClient) r, + Member (E.FederationAPIAccess FederatorClient) r, Member TeamStore r, Member P.TinyLog r, Member TeamCollaboratorsSubsystem r, @@ -811,13 +813,13 @@ getMLSOne2OneConversationInternal lself qother = getMLSOne2OneConversationV6 :: forall r. ( Member BrigAPIAccess r, - Member ConversationStore r, - Member (Input Env) r, + Member ConversationStore.ConversationStore r, + Member (Input (Maybe (MLSKeysByPurpose MLSPrivateKeys))) r, Member (Error FederationError) r, Member (Error InternalError) r, Member (ErrorS 'MLSNotEnabled) r, Member (ErrorS 'NotConnected) r, - Member (FederationAPIAccess FederatorClient) r, + Member (E.FederationAPIAccess FederatorClient) r, Member TeamStore r, Member P.TinyLog r, Member TeamCollaboratorsSubsystem r, @@ -838,13 +840,13 @@ getMLSOne2OneConversationV6 lself qother = do getMLSOne2OneConversation :: ( Member BrigAPIAccess r, - Member ConversationStore r, - Member (Input Env) r, + Member ConversationStore.ConversationStore r, + Member (Input (Maybe (MLSKeysByPurpose MLSPrivateKeys))) r, Member (Error FederationError) r, Member (Error InternalError) r, Member (ErrorS 'MLSNotEnabled) r, Member (ErrorS 'NotConnected) r, - Member (FederationAPIAccess FederatorClient) r, + Member (E.FederationAPIAccess FederatorClient) r, Member TeamStore r, Member P.TinyLog r, Member TeamCollaboratorsSubsystem r, @@ -860,17 +862,17 @@ getMLSOne2OneConversation lself qother fmt = do <$> formatPublicKeys fmt convWithUnformattedKeys.publicKeys getLocalMLSOne2OneConversation :: - ( Member ConversationStore r, + ( Member ConversationStore.ConversationStore r, Member (Error InternalError) r, Member P.TinyLog r, - Member (Input Env) r, + Member (Input (Maybe (MLSKeysByPurpose MLSPrivateKeys))) r, Member (ErrorS MLSNotEnabled) r ) => Local UserId -> Local ConvId -> Sem r (MLSOne2OneConversation MLSPublicKey) getLocalMLSOne2OneConversation lself lconv = do - mconv <- E.getConversation (tUnqualified lconv) + mconv <- ConversationStore.getConversation (tUnqualified lconv) keys <- mlsKeysToPublic <$$> getMLSPrivateKeys conv <- case mconv of Nothing -> pure (localMLSOne2OneConversation lself lconv) @@ -885,7 +887,7 @@ getRemoteMLSOne2OneConversation :: ( Member (Error InternalError) r, Member (Error FederationError) r, Member (ErrorS 'NotConnected) r, - Member (FederationAPIAccess FederatorClient) r, + Member (E.FederationAPIAccess FederatorClient) r, Member (ErrorS MLSNotEnabled) r, Member TinyLog r ) => @@ -937,13 +939,13 @@ getRemoteMLSOne2OneConversation lself qother rconv = do -- group ID, however we /do/ assume that the two backends agree on which of the -- two is responsible for hosting the conversation. isMLSOne2OneEstablished :: - ( Member ConversationStore r, - Member (Input Env) r, + ( Member ConversationStore.ConversationStore r, + Member (Input (Maybe (MLSKeysByPurpose MLSPrivateKeys))) r, Member (Error FederationError) r, Member (Error InternalError) r, Member (ErrorS 'MLSNotEnabled) r, Member (ErrorS 'NotConnected) r, - Member (FederationAPIAccess FederatorClient) r, + Member (E.FederationAPIAccess FederatorClient) r, Member TinyLog r ) => Local UserId -> @@ -959,11 +961,11 @@ isMLSOne2OneEstablished lself qother = do convId isLocalMLSOne2OneEstablished :: - (Member ConversationStore r) => + (Member ConversationStore.ConversationStore r) => Local ConvId -> Sem r Bool isLocalMLSOne2OneEstablished lconv = do - mconv <- E.getConversation (tUnqualified lconv) + mconv <- ConversationStore.getConversation (tUnqualified lconv) pure $ case mconv of Nothing -> False Just conv -> do @@ -974,7 +976,7 @@ isRemoteMLSOne2OneEstablished :: ( Member (ErrorS 'NotConnected) r, Member (Error FederationError) r, Member (Error InternalError) r, - Member (FederationAPIAccess FederatorClient) r, + Member (E.FederationAPIAccess FederatorClient) r, Member (ErrorS MLSNotEnabled) r, Member TinyLog r ) => @@ -993,7 +995,7 @@ isRemoteMLSOne2OneEstablished lself qother rconv = do ep = epochNumber . cnvmlsEpoch searchChannels :: - ( Member ConversationStore r, + ( Member ConversationStore.ConversationStore r, Member (ErrorS NotATeamMember) r, Member (ErrorS OperationDenied) r, Member TeamSubsystem r @@ -1015,8 +1017,8 @@ searchChannels lusr tid searchString sortOrder pageSize lastName lastId discover Left e | not discoverable -> throw e _ -> pure () ConversationPage - <$> E.searchConversations - E.ConversationSearch + <$> ConversationStore.searchConversations + ConversationStore.ConversationSearch { team = tid, searchString, sortOrder = fromMaybe Desc sortOrder, diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index fa73ab30a53..e8ce6365e85 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -82,10 +82,7 @@ import Galley.API.Teams.Features.Get import Galley.API.Teams.Notifications qualified as APITeamQueue import Galley.API.Update qualified as API import Galley.App -import Galley.Effects import Galley.Effects.Queue qualified as E -import Galley.Effects.SearchVisibilityStore qualified as SearchVisibilityData -import Galley.Effects.TeamMemberStore qualified as E import Galley.Options import Galley.Types.Error as Galley import Imports hiding (forkIO) @@ -96,14 +93,12 @@ import Polysemy.TinyLog qualified as P import System.Logger qualified as Log import Wire.API.Conversation (ConvType (..), ConversationRemoveMembers (..)) import Wire.API.Conversation qualified -import Wire.API.Conversation.Config (ConversationSubsystemConfig) 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.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 @@ -128,13 +123,16 @@ import Wire.API.User qualified as U import Wire.BrigAPIAccess qualified as Brig import Wire.BrigAPIAccess qualified as E import Wire.CodeStore +import Wire.ConversationStore (ConversationStore) import Wire.ConversationStore qualified as E import Wire.ConversationSubsystem import Wire.ConversationSubsystem.Util import Wire.FeaturesConfigSubsystem -import Wire.FederationSubsystem +import Wire.LegalHoldStore (LegalHoldStore) +import Wire.ListItems import Wire.ListItems qualified as E import Wire.NotificationSubsystem +import Wire.ProposalStore (ProposalStore) import Wire.Sem.Now import Wire.Sem.Now qualified as Now import Wire.Sem.Paging.Cassandra @@ -142,6 +140,9 @@ import Wire.StoredConversation import Wire.TeamCollaboratorsSubsystem import Wire.TeamJournal (TeamJournal) import Wire.TeamJournal qualified as Journal +import Wire.TeamMemberStore qualified as E +import Wire.TeamNotificationStore (TeamNotificationStore) +import Wire.TeamStore (TeamStore) import Wire.TeamStore qualified as E import Wire.TeamSubsystem (TeamSubsystem) import Wire.TeamSubsystem qualified as TeamSubsystem @@ -149,7 +150,7 @@ import Wire.UserList getTeamH :: forall r. - (Member (ErrorS 'TeamNotFound) r, Member (Queue DeleteItem) r, Member TeamStore r, Member TeamSubsystem r) => + (Member (ErrorS 'TeamNotFound) r, Member (E.Queue DeleteItem) r, Member TeamStore r, Member TeamSubsystem r) => UserId -> TeamId -> Sem r Public.Team @@ -192,7 +193,7 @@ getTeamNameInternal = fmap (fmap TeamName) . E.getTeamName -- one.) getManyTeams :: ( Member TeamStore r, - Member (Queue DeleteItem) r, + Member (E.Queue DeleteItem) r, Member (ListItems LegacyPaging TeamId) r, Member TeamSubsystem r ) => @@ -205,7 +206,7 @@ getManyTeams zusr = lookupTeam :: ( Member TeamStore r, - Member (Queue DeleteItem) r, + Member (E.Queue DeleteItem) r, Member TeamSubsystem r ) => UserId -> @@ -258,7 +259,7 @@ createBindingTeam tid zusr body = do pure tid updateTeamStatus :: - ( Member BrigAPIAccess r, + ( Member E.BrigAPIAccess r, Member (ErrorS 'InvalidTeamStatusUpdate) r, Member (ErrorS 'TeamNotFound) r, Member Now r, @@ -330,13 +331,13 @@ updateTeamH zusr zcon tid updateData = do deleteTeam :: forall r. - ( Member BrigAPIAccess r, + ( Member E.BrigAPIAccess r, Member (Error AuthenticationError) r, Member (ErrorS 'DeleteQueueFull) r, Member (ErrorS 'NotATeamMember) r, Member (ErrorS OperationDenied) r, Member (ErrorS 'TeamNotFound) r, - Member (Queue DeleteItem) r, + Member (E.Queue DeleteItem) r, Member TeamStore r, Member TeamSubsystem r ) => @@ -366,7 +367,7 @@ internalDeleteBindingTeam :: Member (ErrorS 'TeamNotFound) r, Member (ErrorS 'NotAOneMemberTeam) r, Member (ErrorS 'DeleteQueueFull) r, - Member (Queue DeleteItem) r, + Member (E.Queue DeleteItem) r, Member TeamStore r, Member TeamSubsystem r ) => @@ -401,8 +402,8 @@ getTeamConversationRoles zusr tid = do getTeamMembers :: ( Member (ErrorS 'NotATeamMember) r, - Member BrigAPIAccess r, - Member (TeamMemberStore CassandraPaging) r, + Member E.BrigAPIAccess r, + Member (E.TeamMemberStore CassandraPaging) r, Member P.TinyLog r, Member TeamSubsystem r ) => @@ -511,7 +512,7 @@ uncheckedGetTeamMember tid uid = addTeamMember :: forall r. - ( Member BrigAPIAccess r, + ( Member E.BrigAPIAccess r, Member NotificationSubsystem r, Member (ErrorS 'InvalidPermissions) r, Member (ErrorS 'NoAddToBinding) r, @@ -561,7 +562,7 @@ addTeamMember lzusr zcon tid nmem = do -- This function is "unchecked" because there is no need to check for user binding (invite only). uncheckedAddTeamMember :: forall r. - ( Member BrigAPIAccess r, + ( Member E.BrigAPIAccess r, Member NotificationSubsystem r, Member (ErrorS 'TooManyTeamMembers) r, Member (ErrorS 'TooManyTeamAdmins) r, @@ -589,7 +590,7 @@ uncheckedAddTeamMember tid nmem = do uncheckedUpdateTeamMember :: forall r. - ( Member BrigAPIAccess r, + ( Member E.BrigAPIAccess r, Member (ErrorS 'TeamNotFound) r, Member (ErrorS 'TeamMemberNotFound) r, Member (ErrorS 'TooManyTeamAdmins) r, @@ -646,7 +647,7 @@ uncheckedUpdateTeamMember mlzusr mZcon tid newMem = do updateTeamMember :: forall r. - ( Member BrigAPIAccess r, + ( Member E.BrigAPIAccess r, Member (ErrorS 'AccessDenied) r, Member (ErrorS 'InvalidPermissions) r, Member (ErrorS 'TeamNotFound) r, @@ -700,7 +701,7 @@ updateTeamMember lzusr zcon tid newMem = do && permissionsRole targetPermissions /= Just RoleOwner deleteTeamMember :: - ( Member BrigAPIAccess r, + ( Member E.BrigAPIAccess r, Member ConversationStore r, Member (Error AuthenticationError) r, Member (Error InvalidInput) r, @@ -728,7 +729,7 @@ deleteTeamMember :: deleteTeamMember lusr zcon tid remove body = deleteTeamMember' lusr zcon tid remove (Just body) deleteNonBindingTeamMember :: - ( Member BrigAPIAccess r, + ( Member E.BrigAPIAccess r, Member ConversationStore r, Member (Error AuthenticationError) r, Member (Error InvalidInput) r, @@ -756,7 +757,7 @@ deleteNonBindingTeamMember lusr zcon tid remove = deleteTeamMember' lusr zcon ti -- | 'TeamMemberDeleteData' is only required for binding teams deleteTeamMember' :: - ( Member BrigAPIAccess r, + ( Member E.BrigAPIAccess r, Member ConversationStore r, Member (Error AuthenticationError) r, Member (Error InvalidInput) r, @@ -962,24 +963,16 @@ getTeamConversation zusr tid cid = do pure $ newTeamConversation teamConv deleteTeamConversation :: - ( Member BackendNotificationQueueAccess r, - Member BrigAPIAccess r, - Member CodeStore r, + ( Member CodeStore r, Member ConversationStore r, Member (Error FederationError) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member (ErrorS 'NotATeamMember) r, Member (ErrorS ('ActionDenied 'Public.DeleteConversation)) r, - Member (FederationAPIAccess FederatorClient) r, Member ProposalStore r, Member ConversationSubsystem r, - Member TeamStore r, - Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r, - Member FederationSubsystem r, - Member TeamSubsystem r, - Member (Input ConversationSubsystemConfig) r + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -993,7 +986,7 @@ deleteTeamConversation lusr zcon _tid cid = do getSearchVisibility :: ( Member (ErrorS 'NotATeamMember) r, Member (ErrorS OperationDenied) r, - Member SearchVisibilityStore r, + Member TeamStore r, Member TeamSubsystem r ) => Local UserId -> @@ -1009,7 +1002,7 @@ setSearchVisibility :: ( Member (ErrorS 'NotATeamMember) r, Member (ErrorS OperationDenied) r, Member (ErrorS 'TeamSearchVisibilityNotEnabled) r, - Member SearchVisibilityStore r, + Member TeamStore r, Member TeamSubsystem r ) => (TeamId -> Sem r Bool) -> @@ -1092,7 +1085,7 @@ ensureNotElevated targetPermissions member = $ throwS @'InvalidPermissions ensureNotTooLarge :: - ( Member BrigAPIAccess r, + ( Member E.BrigAPIAccess r, Member (ErrorS 'TooManyTeamMembers) r, Member (Input Opts) r ) => @@ -1131,7 +1124,7 @@ ensureNotTooLargeForLegalHold tid teamSize = throwS @'TooManyTeamMembersOnTeamWithLegalhold addTeamMemberInternal :: - ( Member BrigAPIAccess r, + ( Member E.BrigAPIAccess r, Member (ErrorS 'TooManyTeamMembers) r, Member (ErrorS 'TooManyTeamAdmins) r, Member NotificationSubsystem r, @@ -1204,7 +1197,7 @@ getBindingTeamMembers zusr = do -- RegisterError`. canUserJoinTeam :: forall r. - ( Member BrigAPIAccess r, + ( Member E.BrigAPIAccess r, Member LegalHoldStore r, Member (ErrorS 'TooManyTeamMembersOnTeamWithLegalhold) r, Member (Input FanoutLimit) r, @@ -1221,17 +1214,17 @@ canUserJoinTeam tid = do -- | Modify and get visibility type for a team (internal, no user permission checks) getSearchVisibilityInternal :: - (Member SearchVisibilityStore r) => + (Member TeamStore r) => TeamId -> Sem r TeamSearchVisibilityView getSearchVisibilityInternal = fmap TeamSearchVisibilityView - . SearchVisibilityData.getSearchVisibility + . E.getSearchVisibility setSearchVisibilityInternal :: forall r. ( Member (ErrorS 'TeamSearchVisibilityNotEnabled) r, - Member SearchVisibilityStore r + Member TeamStore r ) => (TeamId -> Sem r Bool) -> TeamId -> @@ -1240,7 +1233,7 @@ setSearchVisibilityInternal :: setSearchVisibilityInternal availableForTeam tid (TeamSearchVisibilityView searchVisibility) = do unlessM (availableForTeam tid) $ throwS @'TeamSearchVisibilityNotEnabled - SearchVisibilityData.setSearchVisibility tid searchVisibility + E.setSearchVisibility tid searchVisibility userIsTeamOwner :: ( Member (ErrorS 'TeamMemberNotFound) r, @@ -1260,7 +1253,7 @@ userIsTeamOwner tid uid = do -- Queues a team for async deletion queueTeamDeletion :: ( Member (ErrorS 'DeleteQueueFull) r, - Member (Queue DeleteItem) r + Member (E.Queue DeleteItem) r ) => TeamId -> UserId -> diff --git a/services/galley/src/Galley/API/Teams/Export.hs b/services/galley/src/Galley/API/Teams/Export.hs index 51c99e5fbdb..114532e9f04 100644 --- a/services/galley/src/Galley/API/Teams/Export.hs +++ b/services/galley/src/Galley/API/Teams/Export.hs @@ -29,8 +29,6 @@ import Data.IORef (atomicModifyIORef, newIORef) import Data.Id import Data.Map qualified as Map import Data.Qualified (Local, tUnqualified) -import Galley.Effects -import Galley.Effects.TeamMemberStore (listTeamMembers) import Imports hiding (atomicModifyIORef, newEmptyMVar, newIORef, putMVar, readMVar, takeMVar, threadDelay, tryPutMVar) import Polysemy import Polysemy.Async @@ -47,6 +45,7 @@ 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.TeamMemberStore (TeamMemberStore, listTeamMembers) import Wire.TeamSubsystem (TeamSubsystem) import Wire.TeamSubsystem qualified as TeamSubsystem @@ -85,7 +84,7 @@ lookupInviter cache uid = flip onException ensureCache $ do getUserRecord :: ( Member BrigAPIAccess r, - Member SparAPIAccess r, + Member Spar.SparAPIAccess r, Member (ErrorS TeamMemberNotFound) r, Member (Final IO) r, Member Resource r @@ -123,7 +122,7 @@ getTeamMembersCSV :: Member (ErrorS 'AccessDenied) r, Member (TeamMemberStore InternalPaging) r, Member (Final IO) r, - Member SparAPIAccess r, + Member Spar.SparAPIAccess r, Member TeamSubsystem r ) => Local UserId -> diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index e77605c31e1..0e702ace164 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -45,8 +45,6 @@ import Galley.API.LegalHold qualified as LegalHold import Galley.API.LegalHold.Team qualified as LegalHold import Galley.API.Teams.Features.Get import Galley.App -import Galley.Effects -import Galley.Effects.SearchVisibilityStore qualified as SearchVisibilityData import Galley.Options import Galley.Types.Error (InternalError) import Imports @@ -65,21 +63,31 @@ import Wire.API.Federation.Error import Wire.API.Team.Feature import Wire.API.Team.FeatureFlags import Wire.API.Team.Member -import Wire.BrigAPIAccess (updateSearchVisibilityInbound) +import Wire.BackendNotificationQueueAccess +import Wire.BrigAPIAccess (BrigAPIAccess, updateSearchVisibilityInbound) import Wire.CodeStore -import Wire.ConversationStore (MLSCommitLockStore) +import Wire.ConversationStore (ConversationStore, MLSCommitLockStore) import Wire.ConversationSubsystem import Wire.ConversationSubsystem.Util (assertTeamExists, getTeamMembersForFanout, permissionCheck) +import Wire.ExternalAccess (ExternalAccess) import Wire.FeaturesConfigSubsystem (FeaturesConfigSubsystem, getDbFeatureRawInternal) import Wire.FeaturesConfigSubsystem.Types (GetFeatureConfigEffects) import Wire.FeaturesConfigSubsystem.Utils (resolveServerFeature) +import Wire.FederationAPIAccess (FederationAPIAccess) import Wire.FederationSubsystem (FederationSubsystem) +import Wire.FireAndForget +import Wire.LegalHoldStore (LegalHoldStore) import Wire.NotificationSubsystem +import Wire.ProposalStore (ProposalStore) import Wire.Sem.Now (Now) import Wire.Sem.Paging import Wire.Sem.Paging.Cassandra +import Wire.Sem.Random (Random) import Wire.TeamCollaboratorsSubsystem import Wire.TeamFeatureStore +import Wire.TeamMemberStore (TeamMemberStore) +import Wire.TeamStore (TeamStore) +import Wire.TeamStore qualified as SearchVisibilityData import Wire.TeamSubsystem (TeamSubsystem) import Wire.TeamSubsystem qualified as TeamSubsystem @@ -305,7 +313,7 @@ instance SetFeatureConfig SSOConfig where instance SetFeatureConfig SearchVisibilityAvailableConfig where type SetFeatureForTeamConstraints SearchVisibilityAvailableConfig (r :: EffectRow) = - ( Member SearchVisibilityStore r, + ( Member TeamStore r, Member (Input Opts) r ) @@ -314,7 +322,7 @@ instance SetFeatureConfig SearchVisibilityAvailableConfig where FeatureStatusEnabled -> pure () FeatureStatusDisabled -> SearchVisibilityData.resetSearchVisibility tid -instance SetFeatureConfig ValidateSAMLEmailsConfig +instance SetFeatureConfig RequireExternalEmailVerificationConfig instance SetFeatureConfig DigitalSignaturesConfig diff --git a/services/galley/src/Galley/API/Teams/Features/Get.hs b/services/galley/src/Galley/API/Teams/Features/Get.hs index b22a9bff7b0..231aa5d3c85 100644 --- a/services/galley/src/Galley/API/Teams/Features/Get.hs +++ b/services/galley/src/Galley/API/Teams/Features/Get.hs @@ -37,7 +37,6 @@ import Control.Error (hush) import Data.Id import Data.SOP import Data.Tagged -import Galley.Effects import Imports import Polysemy import Polysemy.Error @@ -60,7 +59,7 @@ data DoAuth = DoAuth UserId | DontDoAuth getFeatureInternal :: ( GetFeatureConfig cfg, Member (ErrorS 'TeamNotFound) r, - Member TeamStore r, + Member TeamStore.TeamStore r, Member FeaturesConfigSubsystem r ) => TeamId -> @@ -73,7 +72,7 @@ toTeamStatus :: TeamId -> LockableFeature cfg -> Multi.TeamStatus cfg toTeamStatus tid feat = Multi.TeamStatus tid feat.status getTeamAndCheckMembership :: - ( Member TeamStore r, + ( Member TeamStore.TeamStore r, Member (ErrorS 'NotATeamMember) r, Member (ErrorS 'TeamNotFound) r, Member TeamSubsystem r @@ -99,7 +98,7 @@ getAllTeamFeaturesForUser :: forall r. ( Member (ErrorS 'NotATeamMember) r, Member (ErrorS 'TeamNotFound) r, - Member TeamStore r, + Member TeamStore.TeamStore r, Member TeamSubsystem r, Member FeaturesConfigSubsystem r, GetFeatureConfigEffects r @@ -117,7 +116,7 @@ getSingleFeatureForUser :: ( GetFeatureConfig cfg, Member (ErrorS 'NotATeamMember) r, Member (ErrorS 'TeamNotFound) r, - Member TeamStore r, + Member TeamStore.TeamStore r, Member TeamSubsystem r, Member FeaturesConfigSubsystem r ) => @@ -135,7 +134,7 @@ getSingleFeatureForUser uid = do guardSecondFactorDisabled :: forall r. ( Member (ErrorS 'AccessDenied) r, - Member TeamStore r, + Member TeamStore.TeamStore r, Member ConversationStore r, Member FeaturesConfigSubsystem r ) => @@ -158,7 +157,7 @@ featureEnabledForTeam :: forall cfg r. ( GetFeatureConfig cfg, Member (ErrorS 'TeamNotFound) r, - Member TeamStore r, + Member TeamStore.TeamStore r, Member FeaturesConfigSubsystem r ) => TeamId -> diff --git a/services/galley/src/Galley/API/Teams/Notifications.hs b/services/galley/src/Galley/API/Teams/Notifications.hs index 6f67d93b09f..4cb01cf9ee7 100644 --- a/services/galley/src/Galley/API/Teams/Notifications.hs +++ b/services/galley/src/Galley/API/Teams/Notifications.hs @@ -43,9 +43,6 @@ import Data.Id import Data.Json.Util (toJSONObject) import Data.List.NonEmpty qualified as NonEmpty import Data.Range (Range) -import Galley.Data.TeamNotifications qualified as DataTeamQueue -import Galley.Effects -import Galley.Effects.TeamNotificationStore qualified as E import Imports import Polysemy import Wire.API.Error @@ -54,11 +51,12 @@ import Wire.API.Event.Team (Event) import Wire.API.Internal.Notification import Wire.API.User import Wire.BrigAPIAccess as Intra +import Wire.TeamNotificationStore qualified as E getTeamNotifications :: ( Member BrigAPIAccess r, Member (ErrorS 'TeamNotFound) r, - Member TeamNotificationStore r + Member E.TeamNotificationStore r ) => UserId -> Maybe NotificationId -> @@ -69,11 +67,11 @@ getTeamNotifications zusr since size = do page <- E.getTeamNotifications tid since size pure $ queuedNotificationList - (toList (DataTeamQueue.resultSeq page)) - (DataTeamQueue.resultHasMore page) + (toList (E.resultSeq page)) + (E.resultHasMore page) Nothing -pushTeamEvent :: (Member TeamNotificationStore r) => TeamId -> Event -> Sem r () +pushTeamEvent :: (Member E.TeamNotificationStore r) => TeamId -> Event -> Sem r () pushTeamEvent tid evt = do nid <- E.mkNotificationId E.createTeamNotification tid nid (NonEmpty.singleton $ toJSONObject evt) diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index 8df0cb3c142..b214caeee61 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -97,7 +97,6 @@ import Galley.API.Message import Galley.API.Query qualified as Query import Galley.API.Teams.Features.Get import Galley.App -import Galley.Effects import Galley.Options import Galley.Types.Error import Imports hiding (forkIO) @@ -132,9 +131,12 @@ import Wire.API.Team.FeatureFlags (FanoutLimit) import Wire.API.Team.Member import Wire.API.User.Client import Wire.API.UserGroup +import Wire.BackendNotificationQueueAccess +import Wire.BrigAPIAccess (BrigAPIAccess) import Wire.CodeStore (CodeStore) import Wire.CodeStore qualified as E import Wire.CodeStore.Code +import Wire.ConversationStore (ConversationStore) import Wire.ConversationStore qualified as E import Wire.ConversationSubsystem import Wire.ConversationSubsystem.Util @@ -142,13 +144,18 @@ import Wire.ExternalAccess qualified as E import Wire.FeaturesConfigSubsystem import Wire.FederationAPIAccess qualified as E import Wire.FederationSubsystem +import Wire.FireAndForget import Wire.HashPassword as HashPassword +import Wire.LegalHoldStore (LegalHoldStore) import Wire.NotificationSubsystem +import Wire.ProposalStore (ProposalStore) import Wire.RateLimit import Wire.Sem.Now (Now) import Wire.Sem.Now qualified as Now +import Wire.Sem.Random (Random) import Wire.StoredConversation import Wire.TeamCollaboratorsSubsystem +import Wire.TeamStore import Wire.TeamSubsystem (TeamSubsystem) import Wire.TeamSubsystem qualified as TeamSubsystem import Wire.UserClientIndexStore qualified as E @@ -279,8 +286,8 @@ type UpdateConversationAccessEffects = ErrorS 'ConvNotFound, ErrorS 'InvalidOperation, ErrorS 'InvalidTargetAccess, - ExternalAccess, - FederationAPIAccess FederatorClient, + E.ExternalAccess, + E.FederationAPIAccess FederatorClient, FireAndForget, NotificationSubsystem, ConversationSubsystem, @@ -295,9 +302,6 @@ type UpdateConversationAccessEffects = updateConversationAccess :: ( Members UpdateConversationAccessEffects r, Member Now r, - Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r, - Member FederationSubsystem r, Member TeamSubsystem r ) => Local UserId -> @@ -308,7 +312,7 @@ updateConversationAccess :: updateConversationAccess lusr con qcnv update = do lcnv <- ensureLocal lusr qcnv getUpdateResult . fmap lcuEvent $ - updateLocalConversation @'ConversationAccessDataTag lcnv (tUntagged lusr) (Just con) update + updateLocalConversationAccessData lcnv (tUntagged lusr) (Just con) update updateConversationHistory :: ( Member (Error FederationError) r, @@ -316,13 +320,8 @@ updateConversationHistory :: Member (ErrorS InvalidOperation) r, Member (ErrorS ConvNotFound) r, Member (ErrorS HistoryNotSupported) r, - Member BackendNotificationQueueAccess r, - Member (Input ConversationSubsystemConfig) r, Member ConversationStore r, Member ConversationSubsystem r, - Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r, - Member FederationSubsystem r, Member TeamSubsystem r ) => Local UserId -> @@ -333,8 +332,7 @@ updateConversationHistory :: updateConversationHistory lusr con qcnv update = do lcnv <- ensureLocal lusr qcnv getUpdateResult . fmap lcuEvent $ - updateLocalConversation - @'ConversationHistoryUpdateTag + updateLocalConversationHistoryUpdate lcnv (tUntagged lusr) (Just con) @@ -343,9 +341,6 @@ updateConversationHistory lusr con qcnv update = do updateConversationAccessUnqualified :: ( Members UpdateConversationAccessEffects r, Member Now r, - Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r, - Member FederationSubsystem r, Member TeamSubsystem r ) => Local UserId -> @@ -355,15 +350,14 @@ updateConversationAccessUnqualified :: Sem r (UpdateResult Event) updateConversationAccessUnqualified lusr con cnv update = getUpdateResult . fmap lcuEvent $ - updateLocalConversation @'ConversationAccessDataTag + updateLocalConversationAccessData (qualifyAs lusr cnv) (tUntagged lusr) (Just con) update updateConversationReceiptMode :: - ( Member BackendNotificationQueueAccess r, - Member BrigAPIAccess r, + ( Member BrigAPIAccess r, Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, @@ -371,17 +365,13 @@ updateConversationReceiptMode :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member (ErrorS 'MLSReadReceiptsNotAllowed) r, - Member ExternalAccess r, - Member (FederationAPIAccess FederatorClient) r, + Member E.ExternalAccess r, + Member (E.FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, Member (Input (Local ())) r, Member TinyLog r, - Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r, - Member FederationSubsystem r, - Member TeamSubsystem r, - Member (Input ConversationSubsystemConfig) r + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -395,8 +385,7 @@ updateConversationReceiptMode lusr zcon qcnv update = lusr ( \lcnv -> getUpdateResult . fmap lcuEvent $ - updateLocalConversation - @'ConversationReceiptModeUpdateTag + updateLocalConversationReceiptModeUpdate lcnv (tUntagged lusr) (Just zcon) @@ -408,8 +397,8 @@ updateConversationReceiptMode lusr zcon qcnv update = updateRemoteConversation :: forall tag r. ( Member BrigAPIAccess r, - Member ExternalAccess r, - Member (FederationAPIAccess FederatorClient) r, + Member E.ExternalAccess r, + Member (E.FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member (Input (Local ())) r, Member ConversationStore r, @@ -445,8 +434,7 @@ updateRemoteConversation rcnv lusr mconn action = getUpdateResult $ do updateLocalStateOfRemoteConv (qualifyAs rcnv convUpdate) mconn >>= note NoChanges updateConversationReceiptModeUnqualified :: - ( Member BackendNotificationQueueAccess r, - Member BrigAPIAccess r, + ( Member BrigAPIAccess r, Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, @@ -454,17 +442,13 @@ updateConversationReceiptModeUnqualified :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member (ErrorS 'MLSReadReceiptsNotAllowed) r, - Member ExternalAccess r, - Member (FederationAPIAccess FederatorClient) r, + Member E.ExternalAccess r, + Member (E.FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, Member (Input (Local ())) r, Member TinyLog r, - Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r, - Member FederationSubsystem r, - Member TeamSubsystem r, - Member (Input ConversationSubsystemConfig) r + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -474,18 +458,13 @@ updateConversationReceiptModeUnqualified :: updateConversationReceiptModeUnqualified lusr zcon cnv = updateConversationReceiptMode lusr zcon (tUntagged (qualifyAs lusr cnv)) updateConversationMessageTimer :: - ( Member BackendNotificationQueueAccess r, - Member ConversationStore r, + ( Member ConversationStore r, Member (ErrorS ('ActionDenied 'ModifyConversationMessageTimer)) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member (Error FederationError) r, Member ConversationSubsystem r, - Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r, - Member FederationSubsystem r, - Member TeamSubsystem r, - Member (Input ConversationSubsystemConfig) r + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -498,8 +477,7 @@ updateConversationMessageTimer lusr zcon qcnv update = lusr ( \lcnv -> lcuEvent - <$> updateLocalConversation - @'ConversationMessageTimerUpdateTag + <$> updateLocalConversationMessageTimerUpdate lcnv (tUntagged lusr) (Just zcon) @@ -509,18 +487,13 @@ updateConversationMessageTimer lusr zcon qcnv update = qcnv updateConversationMessageTimerUnqualified :: - ( Member BackendNotificationQueueAccess r, - Member ConversationStore r, + ( Member ConversationStore r, Member (ErrorS ('ActionDenied 'ModifyConversationMessageTimer)) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member (Error FederationError) r, Member ConversationSubsystem r, - Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r, - Member FederationSubsystem r, - Member TeamSubsystem r, - Member (Input ConversationSubsystemConfig) r + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -530,24 +503,16 @@ updateConversationMessageTimerUnqualified :: updateConversationMessageTimerUnqualified lusr zcon cnv = updateConversationMessageTimer lusr zcon (tUntagged (qualifyAs lusr cnv)) deleteLocalConversation :: - ( Member BrigAPIAccess r, - Member BackendNotificationQueueAccess r, - Member CodeStore r, + ( Member CodeStore r, Member ConversationStore r, Member (Error FederationError) r, Member (ErrorS 'NotATeamMember) r, Member (ErrorS ('ActionDenied 'DeleteConversation)) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, - Member (FederationAPIAccess FederatorClient) r, Member ConversationSubsystem r, Member ProposalStore r, - Member TeamStore r, - Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r, - Member FederationSubsystem r, - Member TeamSubsystem r, - Member (Input ConversationSubsystemConfig) r + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -555,7 +520,7 @@ deleteLocalConversation :: Sem r (UpdateResult Event) deleteLocalConversation lusr con lcnv = getUpdateResult . fmap lcuEvent $ - updateLocalConversation @'ConversationDeleteTag lcnv (tUntagged lusr) (Just con) () + updateLocalConversationDelete lcnv (tUntagged lusr) (Just con) addCodeUnqualifiedWithReqBody :: forall r. @@ -565,7 +530,7 @@ addCodeUnqualifiedWithReqBody :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'GuestLinksDisabled) r, Member (ErrorS 'CreateConversationCodeConflict) r, - Member ExternalAccess r, + Member E.ExternalAccess r, Member NotificationSubsystem r, Member (Input (Local ())) r, Member Now r, @@ -591,7 +556,7 @@ addCodeUnqualified :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'GuestLinksDisabled) r, Member (ErrorS 'CreateConversationCodeConflict) r, - Member ExternalAccess r, + Member E.ExternalAccess r, Member NotificationSubsystem r, Member (Input (Local ())) r, Member Now r, @@ -620,7 +585,7 @@ addCode :: Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'GuestLinksDisabled) r, Member (ErrorS 'CreateConversationCodeConflict) r, - Member ExternalAccess r, + Member E.ExternalAccess r, Member HashPassword r, Member NotificationSubsystem r, Member Now r, @@ -673,7 +638,7 @@ rmCodeUnqualified :: Member ConversationStore r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvAccessDenied) r, - Member ExternalAccess r, + Member E.ExternalAccess r, Member NotificationSubsystem r, Member (Input (Local ())) r, Member Now r, @@ -692,7 +657,7 @@ rmCode :: Member ConversationStore r, Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'ConvNotFound) r, - Member ExternalAccess r, + Member E.ExternalAccess r, Member NotificationSubsystem r, Member Now r, Member TeamSubsystem r @@ -768,29 +733,20 @@ updateConversationProtocolWithLocalUser :: Member (ErrorS 'InvalidOperation) r, Member (Error FederationError) r, Member (ErrorS 'MLSMigrationCriteriaNotSatisfied) r, - Member (ErrorS 'NotATeamMember) r, - Member (ErrorS OperationDenied) r, - Member (ErrorS 'TeamNotFound) r, Member (Error InternalError) r, Member Now r, - Member (Input Env) r, Member (Input (Local ())) r, - Member (Input Opts) r, Member BackendNotificationQueueAccess r, Member BrigAPIAccess r, Member ConversationStore r, Member TinyLog r, Member NotificationSubsystem r, Member ConversationSubsystem r, - Member ExternalAccess r, - Member (FederationAPIAccess FederatorClient) r, + Member E.ExternalAccess r, + Member (E.FederationAPIAccess FederatorClient) r, Member Random r, Member ProposalStore r, - Member TeamFeatureStore r, Member FeaturesConfigSubsystem r, - Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r, - Member FederationSubsystem r, Member TeamSubsystem r, Member (Input ConversationSubsystemConfig) r ) => @@ -807,7 +763,7 @@ updateConversationProtocolWithLocalUser lusr conn qcnv (P.ProtocolUpdate newProt ( \lcnv -> do fmap (maybe Unchanged (Updated . lcuEvent) . hush) . runError @NoChanges - . updateLocalConversation @'ConversationUpdateProtocolTag lcnv (tUntagged lusr) (Just conn) + . updateLocalConversationUpdateProtocol lcnv (tUntagged lusr) (Just conn) $ newProtocol ) ( \rcnv -> @@ -817,30 +773,22 @@ updateConversationProtocolWithLocalUser lusr conn qcnv (P.ProtocolUpdate newProt qcnv updateChannelAddPermission :: - ( Member BackendNotificationQueueAccess r, - Member ConversationStore r, + ( Member ConversationStore r, Member (ErrorS ('ActionDenied 'ModifyAddPermission)) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member (Error FederationError) r, - Member ExternalAccess r, + Member E.ExternalAccess r, Member NotificationSubsystem r, Member ConversationSubsystem r, Member (Input (Local ())) r, Member TinyLog r, - Member (ErrorS (MissingPermission Nothing)) r, - Member (ErrorS NotATeamMember) r, - Member (ErrorS TeamNotFound) r, Member (Error NonFederatingBackends) r, Member (Error UnreachableBackends) r, Member BrigAPIAccess r, - Member (FederationAPIAccess FederatorClient) r, + Member (E.FederationAPIAccess FederatorClient) r, Member (ErrorS 'InvalidTargetAccess) r, - Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r, - Member FederationSubsystem r, - Member TeamSubsystem r, - Member (Input ConversationSubsystemConfig) r + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -853,8 +801,7 @@ updateChannelAddPermission lusr zcon qcnv update = ( \lcnv -> getUpdateResult $ lcuEvent - <$> updateLocalConversation - @'ConversationUpdateAddPermissionTag + <$> updateLocalConversationUpdateAddPermission lcnv (tUntagged lusr) (Just zcon) @@ -966,9 +913,7 @@ addMembers :: ( Member BackendNotificationQueueAccess r, Member BrigAPIAccess r, Member ConversationStore r, - Member (Error InternalError) r, Member (ErrorS ('ActionDenied 'AddConversationMember)) r, - Member (ErrorS ('ActionDenied 'LeaveConversation)) r, Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, @@ -978,10 +923,9 @@ addMembers :: Member (ErrorS 'MissingLegalholdConsent) r, Member (ErrorS 'GroupIdVersionNotSupported) r, Member (Error FederationError) r, - Member (Error NonFederatingBackends) r, Member (Error UnreachableBackends) r, - Member ExternalAccess r, - Member (FederationAPIAccess FederatorClient) r, + Member E.ExternalAccess r, + Member (E.FederationAPIAccess FederatorClient) r, Member ConversationSubsystem r, Member NotificationSubsystem r, Member Now r, @@ -991,7 +935,6 @@ addMembers :: Member TeamStore r, Member TinyLog r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r, Member FederationSubsystem r, Member TeamSubsystem r, Member (Input ConversationSubsystemConfig) r @@ -1016,16 +959,14 @@ addMembers lusr zcon qcnv (InviteQualified users role) = do let joinType = mkJoinType conv action = ConversationJoin {..} getUpdateResult . fmap lcuEvent $ - updateLocalConversation @'ConversationJoinTag lcnv (tUntagged lusr) (Just zcon) action + updateLocalConversationJoin lcnv (tUntagged lusr) (Just zcon) action addMembersUnqualifiedV2 :: ( Member BackendNotificationQueueAccess r, Member BrigAPIAccess r, Member ConversationStore r, Member (Error FederationError) r, - Member (Error InternalError) r, Member (ErrorS ('ActionDenied 'AddConversationMember)) r, - Member (ErrorS ('ActionDenied 'LeaveConversation)) r, Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, @@ -1034,10 +975,9 @@ addMembersUnqualifiedV2 :: Member (ErrorS 'TooManyMembers) r, Member (ErrorS 'MissingLegalholdConsent) r, Member (ErrorS 'GroupIdVersionNotSupported) r, - Member (Error NonFederatingBackends) r, Member (Error UnreachableBackends) r, - Member ExternalAccess r, - Member (FederationAPIAccess FederatorClient) r, + Member E.ExternalAccess r, + Member (E.FederationAPIAccess FederatorClient) r, Member ConversationSubsystem r, Member NotificationSubsystem r, Member Now r, @@ -1047,7 +987,6 @@ addMembersUnqualifiedV2 :: Member TeamStore r, Member TinyLog r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r, Member FederationSubsystem r, Member TeamSubsystem r, Member (Input ConversationSubsystemConfig) r @@ -1060,7 +999,7 @@ addMembersUnqualifiedV2 :: addMembersUnqualifiedV2 lusr zcon cnv (InviteQualified users role) = do let lcnv = qualifyAs lusr cnv getUpdateResult . fmap lcuEvent $ - updateLocalConversation @'ConversationJoinTag lcnv (tUntagged lusr) (Just zcon) $ + updateLocalConversationJoin lcnv (tUntagged lusr) (Just zcon) $ ConversationJoin users role def addMembersUnqualified :: @@ -1068,9 +1007,7 @@ addMembersUnqualified :: Member BrigAPIAccess r, Member ConversationStore r, Member (Error FederationError) r, - Member (Error InternalError) r, Member (ErrorS ('ActionDenied 'AddConversationMember)) r, - Member (ErrorS ('ActionDenied 'LeaveConversation)) r, Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, @@ -1079,10 +1016,9 @@ addMembersUnqualified :: Member (ErrorS 'TooManyMembers) r, Member (ErrorS 'MissingLegalholdConsent) r, Member (ErrorS 'GroupIdVersionNotSupported) r, - Member (Error NonFederatingBackends) r, Member (Error UnreachableBackends) r, - Member ExternalAccess r, - Member (FederationAPIAccess FederatorClient) r, + Member E.ExternalAccess r, + Member (E.FederationAPIAccess FederatorClient) r, Member ConversationSubsystem r, Member NotificationSubsystem r, Member Now r, @@ -1093,7 +1029,6 @@ addMembersUnqualified :: Member TinyLog r, Member TeamCollaboratorsSubsystem r, Member FederationSubsystem r, - Member E.MLSCommitLockStore r, Member TeamSubsystem r, Member (Input ConversationSubsystemConfig) r ) => @@ -1114,9 +1049,7 @@ replaceMembers :: ( Member BackendNotificationQueueAccess r, Member BrigAPIAccess r, Member ConversationStore r, - Member (Error InternalError) r, Member (ErrorS ('ActionDenied 'AddConversationMember)) r, - Member (ErrorS ('ActionDenied 'LeaveConversation)) r, Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'ConvNotFound) r, @@ -1127,12 +1060,10 @@ replaceMembers :: Member (ErrorS 'MissingLegalholdConsent) r, Member (ErrorS 'GroupIdVersionNotSupported) r, Member (Error FederationError) r, - Member (Error NonFederatingBackends) r, Member (Error UnreachableBackends) r, - Member ExternalAccess r, - Member (FederationAPIAccess FederatorClient) r, + Member E.ExternalAccess r, + Member (E.FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, - Member (Input Env) r, Member Now r, Member LegalHoldStore r, Member ProposalStore r, @@ -1140,7 +1071,6 @@ replaceMembers :: Member TeamStore r, Member TinyLog r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r, Member UserGroupStore r, Member ConversationSubsystem r, Member FederationSubsystem r, @@ -1182,7 +1112,7 @@ replaceMembers lusr zcon qcnv (InviteQualified invitedUsers role) = do let joinType = mkJoinType conv action = ConversationJoin {..} getUpdateResult . fmap lcuEvent $ - updateLocalConversation @'ConversationJoinTag lcnv (tUntagged lusr) (Just zcon) action + updateLocalConversationJoin lcnv (tUntagged lusr) (Just zcon) action -- Remove members for_ (nonEmpty $ Set.toList toRemove) $ \removeList -> do @@ -1194,7 +1124,7 @@ replaceMembers lusr zcon qcnv (InviteQualified invitedUsers role) = do for_ removeList $ removeMemberQualified lusr zcon qcnv else void . getUpdateResult . fmap lcuEvent $ - updateLocalConversation @'ConversationRemoveMembersTag + updateLocalConversationRemoveMembers lcnv (tUntagged lusr) (Just zcon) @@ -1203,7 +1133,7 @@ replaceMembers lusr zcon qcnv (InviteQualified invitedUsers role) = do updateSelfMember :: ( Member ConversationStore r, Member (ErrorS 'ConvNotFound) r, - Member ExternalAccess r, + Member E.ExternalAccess r, Member NotificationSubsystem r, Member Now r ) => @@ -1249,7 +1179,7 @@ updateSelfMember lusr zcon qcnv update = do updateUnqualifiedSelfMember :: ( Member ConversationStore r, Member (ErrorS 'ConvNotFound) r, - Member ExternalAccess r, + Member E.ExternalAccess r, Member NotificationSubsystem r, Member Now r ) => @@ -1263,8 +1193,7 @@ updateUnqualifiedSelfMember lusr zcon cnv update = do updateSelfMember lusr zcon (tUntagged lcnv) update updateOtherMemberLocalConv :: - ( Member BackendNotificationQueueAccess r, - Member ConversationStore r, + ( Member ConversationStore r, Member (Error FederationError) r, Member (ErrorS ('ActionDenied 'ModifyOtherConversationMember)) r, Member (ErrorS 'InvalidTarget) r, @@ -1272,11 +1201,7 @@ updateOtherMemberLocalConv :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvMemberNotFound) r, Member ConversationSubsystem r, - Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r, - Member FederationSubsystem r, - Member TeamSubsystem r, - Member (Input ConversationSubsystemConfig) r + Member TeamSubsystem r ) => Local ConvId -> Local UserId -> @@ -1287,12 +1212,11 @@ updateOtherMemberLocalConv :: updateOtherMemberLocalConv lcnv lusr con qvictim update = void . getUpdateResult . fmap lcuEvent $ do when (tUntagged lusr == qvictim) $ throwS @'InvalidTarget - updateLocalConversation @'ConversationMemberUpdateTag lcnv (tUntagged lusr) (Just con) $ + updateLocalConversationMemberUpdate lcnv (tUntagged lusr) (Just con) $ ConversationMemberUpdate qvictim update updateOtherMemberUnqualified :: - ( Member BackendNotificationQueueAccess r, - Member ConversationStore r, + ( Member ConversationStore r, Member (Error FederationError) r, Member (ErrorS ('ActionDenied 'ModifyOtherConversationMember)) r, Member (ErrorS 'InvalidTarget) r, @@ -1300,11 +1224,7 @@ updateOtherMemberUnqualified :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvMemberNotFound) r, Member ConversationSubsystem r, - Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r, - Member FederationSubsystem r, - Member TeamSubsystem r, - Member (Input ConversationSubsystemConfig) r + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -1318,8 +1238,7 @@ updateOtherMemberUnqualified lusr zcon cnv victim update = do updateOtherMemberLocalConv lcnv lusr zcon (tUntagged lvictim) update updateOtherMember :: - ( Member BackendNotificationQueueAccess r, - Member ConversationStore r, + ( Member ConversationStore r, Member (Error FederationError) r, Member (ErrorS ('ActionDenied 'ModifyOtherConversationMember)) r, Member (ErrorS 'InvalidTarget) r, @@ -1327,11 +1246,7 @@ updateOtherMember :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvMemberNotFound) r, Member ConversationSubsystem r, - Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r, - Member FederationSubsystem r, - Member TeamSubsystem r, - Member (Input ConversationSubsystemConfig) r + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -1357,22 +1272,17 @@ removeMemberUnqualified :: ( Member BackendNotificationQueueAccess r, Member ConversationStore r, Member (Error FederationError) r, - Member (Error InternalError) r, Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, - Member ExternalAccess r, - Member (FederationAPIAccess FederatorClient) r, + Member E.ExternalAccess r, + Member (E.FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, - Member (Input Env) r, Member Now r, Member ProposalStore r, Member Random r, Member TinyLog r, - Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r, - Member FederationSubsystem r, Member TeamSubsystem r, Member (Input ConversationSubsystemConfig) r ) => @@ -1390,22 +1300,17 @@ removeMemberQualified :: ( Member BackendNotificationQueueAccess r, Member ConversationStore r, Member (Error FederationError) r, - Member (Error InternalError) r, Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, - Member ExternalAccess r, - Member (FederationAPIAccess FederatorClient) r, + Member E.ExternalAccess r, + Member (E.FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, - Member (Input Env) r, Member Now r, Member ProposalStore r, Member Random r, Member TinyLog r, - Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r, - Member FederationSubsystem r, Member TeamSubsystem r, Member (Input ConversationSubsystemConfig) r ) => @@ -1429,7 +1334,7 @@ pattern EdMembersLeaveRemoved :: QualifiedUserIdList -> EventData pattern EdMembersLeaveRemoved l = EdMembersLeave EdReasonRemoved l removeMemberFromRemoteConv :: - ( Member (FederationAPIAccess FederatorClient) r, + ( Member (E.FederationAPIAccess FederatorClient) r, Member (Error FederationError) r, Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, Member (ErrorS 'ConvNotFound) r, @@ -1470,23 +1375,17 @@ removeMemberFromLocalConv :: ( Member BackendNotificationQueueAccess r, Member ConversationStore r, Member (Error FederationError) r, - Member (Error InternalError) r, Member (ErrorS ('ActionDenied 'LeaveConversation)) r, Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, - Member ExternalAccess r, - Member (FederationAPIAccess FederatorClient) r, + Member E.ExternalAccess r, Member NotificationSubsystem r, Member ConversationSubsystem r, - Member (Input Env) r, Member Now r, Member ProposalStore r, Member Random r, Member TinyLog r, - Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r, - Member FederationSubsystem r, Member TeamSubsystem r, Member (Input ConversationSubsystemConfig) r ) => @@ -1497,10 +1396,9 @@ removeMemberFromLocalConv :: Sem r (Maybe Event) removeMemberFromLocalConv lcnv lusr con victim | tUntagged lusr == victim = - fmap (fmap lcuEvent . hush) - . runError @NoChanges - . updateLocalConversation @'ConversationLeaveTag lcnv (tUntagged lusr) con - $ () + fmap (fmap lcuEvent . hush) $ + runError @NoChanges $ + updateLocalConversationLeave lcnv (tUntagged lusr) con | otherwise = do conv <- getConversationWithError lcnv let lconv = qualifyAs lusr conv @@ -1513,7 +1411,7 @@ removeMemberFromLocalConv lcnv lusr con victim else fmap (fmap lcuEvent . hush) . runError @NoChanges - $ updateLocalConversation @'ConversationRemoveMembersTag + $ updateLocalConversationRemoveMembers lcnv (tUntagged lusr) con @@ -1522,22 +1420,19 @@ removeMemberFromLocalConv lcnv lusr con victim removeMemberFromChannel :: forall r. ( Member (ErrorS 'ConvNotFound) r, - Member (Input Env) r, - Member (Error NoChanges) r, Member ProposalStore r, Member Now r, - Member ExternalAccess r, - Member (FederationAPIAccess FederatorClient) r, + Member E.ExternalAccess r, Member NotificationSubsystem r, Member ConversationSubsystem r, - Member (Error InternalError) r, Member Random r, Member TinyLog r, Member (Error FederationError) r, Member BackendNotificationQueueAccess r, Member ConversationStore r, Member TeamSubsystem r, - Member (Input ConversationSubsystemConfig) r + Member (Input ConversationSubsystemConfig) r, + Member (Error NoChanges) r ) => Qualified UserId -> Local StoredConversation -> @@ -1548,7 +1443,7 @@ removeMemberFromChannel qusr lconv victim = do teamMember <- foldQualified lconv (getTeamMembership conv) (const $ pure Nothing) qusr >>= noteS @'ConvNotFound let action = ConversationRemoveMembers {crmTargets = pure victim, crmReason = EdReasonRemoved} let actorContext = ActorContext (Nothing :: Maybe LocalMember) (Just teamMember) - ensureAllowed @'ConversationRemoveMembersTag (sing @'ConversationRemoveMembersTag) lconv action conv actorContext + ensureAllowed @'ConversationRemoveMembersTag lconv action conv actorContext let notificationTargets = convBotsAndMembers conv kickMember qusr lconv notificationTargets victim where @@ -1559,13 +1454,13 @@ removeMemberFromChannel qusr lconv victim = do postProteusMessage :: ( Member BrigAPIAccess r, - Member UserClientIndexStore r, + Member E.UserClientIndexStore r, Member ConversationStore r, - Member (FederationAPIAccess FederatorClient) r, + Member (E.FederationAPIAccess FederatorClient) r, Member (Error FederationError) r, Member BackendNotificationQueueAccess r, Member NotificationSubsystem r, - Member ExternalAccess r, + Member E.ExternalAccess r, Member (Input Opts) r, Member Now r, Member TinyLog r, @@ -1589,7 +1484,7 @@ postProteusBroadcast :: Member (ErrorS 'NonBindingTeam) r, Member (ErrorS 'BroadcastLimitExceeded) r, Member NotificationSubsystem r, - Member ExternalAccess r, + Member E.ExternalAccess r, Member (Input Opts) r, Member Now r, Member TeamStore r, @@ -1637,10 +1532,10 @@ unqualifyEndpoint loc f ignoreMissing reportMissing message = do postBotMessageUnqualified :: ( Member BrigAPIAccess r, - Member UserClientIndexStore r, + Member E.UserClientIndexStore r, Member ConversationStore r, - Member ExternalAccess r, - Member (FederationAPIAccess FederatorClient) r, + Member E.ExternalAccess r, + Member (E.FederationAPIAccess FederatorClient) r, Member BackendNotificationQueueAccess r, Member NotificationSubsystem r, Member (Input (Local ())) r, @@ -1671,7 +1566,7 @@ postOtrBroadcastUnqualified :: Member (ErrorS 'NonBindingTeam) r, Member (ErrorS 'BroadcastLimitExceeded) r, Member NotificationSubsystem r, - Member ExternalAccess r, + Member E.ExternalAccess r, Member (Input Opts) r, Member Now r, Member TeamStore r, @@ -1693,11 +1588,11 @@ postOtrBroadcastUnqualified sender zcon = postOtrMessageUnqualified :: ( Member BrigAPIAccess r, - Member UserClientIndexStore r, + Member E.UserClientIndexStore r, Member ConversationStore r, - Member (FederationAPIAccess FederatorClient) r, + Member (E.FederationAPIAccess FederatorClient) r, Member BackendNotificationQueueAccess r, - Member ExternalAccess r, + Member E.ExternalAccess r, Member NotificationSubsystem r, Member (Input Opts) r, Member Now r, @@ -1718,20 +1613,14 @@ postOtrMessageUnqualified sender zcon cnv = (runLocalInput sender . postQualifiedOtrMessage User (tUntagged sender) (Just zcon) lcnv) updateConversationName :: - ( Member BackendNotificationQueueAccess r, - Member ConversationStore r, + ( Member ConversationStore r, Member (Error FederationError) r, Member (Error InvalidInput) r, Member (ErrorS ('ActionDenied 'ModifyConversationName)) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member ConversationSubsystem r, - Member TeamStore r, - Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r, - Member FederationSubsystem r, - Member TeamSubsystem r, - Member (Input ConversationSubsystemConfig) r + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -1747,20 +1636,14 @@ updateConversationName lusr zcon qcnv convRename = do convRename updateUnqualifiedConversationName :: - ( Member BackendNotificationQueueAccess r, - Member ConversationStore r, + ( Member ConversationStore r, Member (Error FederationError) r, Member (Error InvalidInput) r, Member (ErrorS ('ActionDenied 'ModifyConversationName)) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member ConversationSubsystem r, - Member TeamStore r, - Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r, - Member FederationSubsystem r, - Member TeamSubsystem r, - Member (Input ConversationSubsystemConfig) r + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -1772,20 +1655,14 @@ updateUnqualifiedConversationName lusr zcon cnv rename = do updateLocalConversationName lusr zcon lcnv rename updateLocalConversationName :: - ( Member BackendNotificationQueueAccess r, - Member ConversationStore r, + ( Member ConversationStore r, Member (Error FederationError) r, Member (Error InvalidInput) r, Member (ErrorS ('ActionDenied 'ModifyConversationName)) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member ConversationSubsystem r, - Member TeamStore r, - Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r, - Member TeamSubsystem r, - Member FederationSubsystem r, - Member (Input ConversationSubsystemConfig) r + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -1794,7 +1671,7 @@ updateLocalConversationName :: Sem r (UpdateResult Event) updateLocalConversationName lusr zcon lcnv rename = getUpdateResult . fmap lcuEvent $ - updateLocalConversation @'ConversationRenameTag lcnv (tUntagged lusr) (Just zcon) rename + updateLocalConversationRename lcnv (tUntagged lusr) (Just zcon) rename memberTyping :: ( Member NotificationSubsystem r, @@ -1802,7 +1679,7 @@ memberTyping :: Member (Input (Local ())) r, Member Now r, Member ConversationStore r, - Member (FederationAPIAccess FederatorClient) r, + Member (E.FederationAPIAccess FederatorClient) r, Member (Error FederationError) r, Member TeamSubsystem r ) => @@ -1841,7 +1718,7 @@ memberTypingUnqualified :: Member (Input (Local ())) r, Member Now r, Member ConversationStore r, - Member (FederationAPIAccess FederatorClient) r, + Member (E.FederationAPIAccess FederatorClient) r, Member (Error FederationError) r, Member TeamSubsystem r ) => @@ -1856,13 +1733,13 @@ memberTypingUnqualified lusr zcon cnv ts = do addBot :: forall r. - ( Member UserClientIndexStore r, + ( Member E.UserClientIndexStore r, Member ConversationStore r, Member (ErrorS ('ActionDenied 'AddConversationMember)) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member (ErrorS 'TooManyMembers) r, - Member ExternalAccess r, + Member E.ExternalAccess r, Member NotificationSubsystem r, Member (Input ConversationSubsystemConfig) r, Member Now r @@ -1923,10 +1800,10 @@ addBot lusr zcon b = do pure (bots, users) rmBot :: - ( Member UserClientIndexStore r, + ( Member E.UserClientIndexStore r, Member ConversationStore r, Member (ErrorS 'ConvNotFound) r, - Member ExternalAccess r, + Member E.ExternalAccess r, Member NotificationSubsystem r, Member Now r, Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index aeb8718c92e..bfa6bbcf79f 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -52,16 +52,8 @@ import Data.Misc import Data.Qualified import Data.Range import Data.Text qualified as Text -import Galley.Cassandra.CustomBackend -import Galley.Cassandra.SearchVisibility -import Galley.Cassandra.Team - ( interpretInternalTeamListToCassandra, - interpretTeamListToCassandra, - interpretTeamMemberStoreToCassandra, - interpretTeamMemberStoreToCassandraWithPaging, - ) -import Galley.Cassandra.TeamNotifications -import Galley.Effects +import Galley.API.MLS.GroupInfoCheck (GroupInfoCheckEnabled (GroupInfoCheckEnabled)) +import Galley.Effects.Queue qualified as GE import Galley.Env import Galley.External.LegalHoldService.Internal qualified as LHInternal import Galley.Keys @@ -86,7 +78,6 @@ import Polysemy.Conc import Polysemy.Error import Polysemy.Fail import Polysemy.Input -import Polysemy.Internal (Append) import Polysemy.Resource import Polysemy.TinyLog (TinyLog, logErrors) import Polysemy.TinyLog qualified as P @@ -99,38 +90,61 @@ import UnliftIO.Exception qualified as UnliftIO import Wire.API.Conversation.Config (ConversationSubsystemConfig (..)) import Wire.API.Conversation.Protocol import Wire.API.Error -import Wire.API.Error.Galley (GalleyError (InvalidOperation), NonFederatingBackends, UnreachableBackends) +import Wire.API.Error.Galley (GalleyError (..), NonFederatingBackends, OperationDenied, UnreachableBackends) +import Wire.API.Federation.Client import Wire.API.Federation.Error +import Wire.API.MLS.Keys (MLSKeysByPurpose, MLSPrivateKeys) import Wire.API.Team.Collaborator import Wire.API.Team.Feature import Wire.API.Team.FeatureFlags import Wire.AWS qualified as Aws +import Wire.BackendNotificationQueueAccess (BackendNotificationQueueAccess) import Wire.BackendNotificationQueueAccess.RabbitMq qualified as BackendNotificationQueueAccess +import Wire.BrigAPIAccess (BrigAPIAccess) import Wire.BrigAPIAccess.Rpc +import Wire.CodeStore (CodeStore) import Wire.CodeStore.Cassandra import Wire.CodeStore.DualWrite import Wire.CodeStore.Postgres +import Wire.ConversationStore (ConversationStore, MLSCommitLockStore) import Wire.ConversationStore.Cassandra import Wire.ConversationStore.Postgres +import Wire.ConversationSubsystem (ConversationSubsystem) import Wire.ConversationSubsystem.Interpreter (interpretConversationSubsystem) +import Wire.CustomBackendStore +import Wire.CustomBackendStore.Cassandra import Wire.Error +import Wire.ExternalAccess (ExternalAccess) import Wire.ExternalAccess.External import Wire.FeaturesConfigSubsystem import Wire.FeaturesConfigSubsystem.Interpreter import Wire.FeaturesConfigSubsystem.Types (ExposeInvitationURLsAllowlist (..)) +import Wire.FederationAPIAccess (FederationAPIAccess) import Wire.FederationAPIAccess.Interpreter +import Wire.FederationSubsystem import Wire.FederationSubsystem.Interpreter (runFederationSubsystem) import Wire.FireAndForget -import Wire.GundeckAPIAccess (runGundeckAPIAccess) +import Wire.GundeckAPIAccess (GundeckAPIAccess, runGundeckAPIAccess) +import Wire.HashPassword import Wire.HashPassword.Interpreter +import Wire.LegalHoldStore (LegalHoldStore) import Wire.LegalHoldStore.Cassandra (interpretLegalHoldStoreToCassandra) import Wire.LegalHoldStore.Env (LegalHoldEnv (..)) +import Wire.ListItems (ListItems) +import Wire.ListItems.Team.Cassandra + ( interpretInternalTeamListToCassandra, + interpretTeamListToCassandra, + ) +import Wire.MeetingsStore (MeetingsStore) import Wire.MeetingsStore.Postgres (interpretMeetingsStoreToPostgres) +import Wire.MeetingsSubsystem (MeetingsSubsystem) import Wire.MeetingsSubsystem.Interpreter qualified as Meeting import Wire.MigrationLock +import Wire.NotificationSubsystem (NotificationSubsystem) import Wire.NotificationSubsystem.Interpreter (runNotificationSubsystemGundeck) import Wire.ParseException import Wire.Postgres (PGConstraints) +import Wire.ProposalStore (ProposalStore) import Wire.ProposalStore.Cassandra import Wire.RateLimit import Wire.RateLimit.Interpreter @@ -138,25 +152,115 @@ import Wire.Rpc import Wire.Sem.Concurrency import Wire.Sem.Concurrency.IO import Wire.Sem.Delay +import Wire.Sem.Now (Now) import Wire.Sem.Now.IO (nowToIO) +import Wire.Sem.Paging.Cassandra +import Wire.Sem.Random (Random) import Wire.Sem.Random.IO +import Wire.ServiceStore (ServiceStore) import Wire.ServiceStore.Cassandra (interpretServiceStoreToCassandra) +import Wire.SparAPIAccess (SparAPIAccess) import Wire.SparAPIAccess.Rpc +import Wire.TeamCollaboratorsStore (TeamCollaboratorsStore) import Wire.TeamCollaboratorsStore.Postgres (interpretTeamCollaboratorsStoreToPostgres) +import Wire.TeamCollaboratorsSubsystem (TeamCollaboratorsSubsystem) import Wire.TeamCollaboratorsSubsystem.Interpreter +import Wire.TeamFeatureStore (TeamFeatureStore) import Wire.TeamFeatureStore.Cassandra import Wire.TeamFeatureStore.Error (TeamFeatureStoreError (..)) import Wire.TeamFeatureStore.Migrating import Wire.TeamFeatureStore.Postgres +import Wire.TeamJournal (TeamJournal) import Wire.TeamJournal.Aws +import Wire.TeamMemberStore (TeamMemberStore) +import Wire.TeamMemberStore.Cassandra + ( interpretTeamMemberStoreToCassandra, + interpretTeamMemberStoreToCassandraWithPaging, + ) +import Wire.TeamNotificationStore (TeamNotificationStore) +import Wire.TeamNotificationStore.Cassandra (interpretTeamNotificationStoreToCassandra) +import Wire.TeamStore (TeamStore) import Wire.TeamStore.Cassandra (interpretTeamStoreToCassandra) +import Wire.TeamSubsystem (TeamSubsystem) import Wire.TeamSubsystem.Interpreter +import Wire.UserClientIndexStore (UserClientIndexStore) import Wire.UserClientIndexStore.Cassandra (interpretUserClientIndexStoreToCassandra) +import Wire.UserGroupStore (UserGroupStore) import Wire.UserGroupStore.Postgres (interpretUserGroupStoreToPostgres) --- Effects needed by the interpretation of other effects -type GalleyEffects0 = - '[ Input ClientState, +type GalleyEffects = + '[ MeetingsSubsystem, + ConversationSubsystem, + FederationSubsystem, + TeamCollaboratorsSubsystem, + Input AllTeamFeatures, + FeaturesConfigSubsystem, + TeamSubsystem, + SparAPIAccess, + NotificationSubsystem, + ExternalAccess, + BrigAPIAccess, + GundeckAPIAccess, + Rpc, + FederationAPIAccess FederatorClient, + BackendNotificationQueueAccess, + FireAndForget, + TeamCollaboratorsStore, + MeetingsStore, + UserClientIndexStore, + CodeStore, + ProposalStore, + RateLimit, + HashPassword, + Random, + CustomBackendStore, + TeamStore, + TeamJournal, + LegalHoldStore, + Input LegalHoldEnv, + UserGroupStore, + ServiceStore, + TeamNotificationStore, + ConversationStore, + MLSCommitLockStore, + TeamFeatureStore, + TeamMemberStore InternalPaging, + TeamMemberStore CassandraPaging, + ListItems LegacyPaging TeamId, + ListItems InternalPaging TeamId, + Input ExposeInvitationURLsAllowlist, + Input FeatureFlags, + Input FanoutLimit, + Input (FeatureDefaults LegalholdConfig), + Input (Local ()), + Input (Maybe (MLSKeysByPurpose MLSPrivateKeys)), + Input (Maybe GroupInfoCheckEnabled), + Input Opts, + Input (Either HttpsUrl (Map Text HttpsUrl)), + Now, + GE.Queue DeleteItem, + Error Meeting.MeetingError, + Error DynError, + Error RateLimitExceeded, + ErrorS OperationDenied, + ErrorS 'HistoryNotSupported, + ErrorS 'NotATeamMember, + ErrorS 'ConvAccessDenied, + ErrorS 'NotConnected, + ErrorS 'MLSNotEnabled, + ErrorS 'MLSNonEmptyMemberList, + ErrorS 'MissingLegalholdConsent, + ErrorS 'NonBindingTeam, + ErrorS 'NoBindingTeamMembers, + ErrorS 'TeamNotFound, + ErrorS 'InvalidOperation, + ErrorS 'ConvNotFound, + ErrorS 'ChannelsNotEnabled, + ErrorS 'NotAnMlsConversation, + ErrorS 'NotATeamMember, + ErrorS 'MeetingNotFound, + ErrorS 'InvalidOperation, + Input ClientState, Input Hasql.Pool, Input Env, Input ConversationSubsystemConfig, @@ -184,8 +288,6 @@ type GalleyEffects0 = Final IO ] -type GalleyEffects = Append GalleyEffects1 GalleyEffects0 - -- Define some invariants for the options used validateOptions :: Opts -> IO (Either HttpsUrl (Map Text HttpsUrl)) validateOptions o = do @@ -407,6 +509,8 @@ evalGalley e = . nowToIO . runInputConst (e ^. convCodeURI) . runInputConst (e ^. options) + . runInputConst (GroupInfoCheckEnabled <$> e._options._settings._checkGroupInfo) + . runInputConst e._mlsKeys . runInputConst localUnit . interpretTeamFeatureSpecialContext e . runInputConst (currentFanoutLimit (e ^. options)) @@ -426,7 +530,6 @@ evalGalley e = . interpretLegalHoldStoreToCassandra lh . interpretTeamJournal (e ^. aEnv) . interpretTeamStoreToCassandra - . interpretSearchVisibilityStoreToCassandra . interpretCustomBackendStoreToCassandra . randomToIO . runHashPassword e._options._settings._passwordHashingOptions diff --git a/services/galley/src/Galley/Cassandra/Queries.hs b/services/galley/src/Galley/Cassandra/Queries.hs deleted file mode 100644 index 789cf8b195c..00000000000 --- a/services/galley/src/Galley/Cassandra/Queries.hs +++ /dev/null @@ -1,145 +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 . - --- | Tables that are used in this module: --- - conversation_codes --- - custom_backend --- - legalhold_pending_prekeys --- - legalhold_service --- - legalhold_whitelisted --- - team --- - team_member --- 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, - upsertCustomBackend, - deleteCustomBackend, - selectSearchVisibility, - updateSearchVisibility, - insertLegalHoldSettings, - selectLegalHoldSettings, - removeLegalHoldSettings, - insertPendingPrekeys, - dropPendingPrekeys, - selectPendingPrekeys, - updateUserLegalHoldStatus, - insertLegalHoldWhitelistedTeam, - removeLegalHoldWhitelistedTeam, - ) -where - -import Cassandra as C hiding (Value) -import Data.Domain (Domain) -import Data.Id -import Data.LegalHold -import Data.Misc -import Imports -import Text.RawString.QQ -import Wire.API.Provider -import Wire.API.Provider.Service -import Wire.API.Team.SearchVisibility -import Wire.API.User.Client.Prekey - --- LegalHold ---------------------------------------------------------------- - -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 = ? - |] - --- Search Visibility -------------------------------------------------------- - -selectSearchVisibility :: PrepQuery R (Identity TeamId) (Identity (Maybe TeamSearchVisibility)) -selectSearchVisibility = - "select search_visibility from team where team = ?" - -updateSearchVisibility :: PrepQuery W (TeamSearchVisibility, TeamId) () -updateSearchVisibility = - {- `IF EXISTS`, but that requires benchmarking -} "update team set search_visibility = ? where team = ?" - --- Custom Backend ----------------------------------------------------------- - -selectCustomBackend :: PrepQuery R (Identity Domain) (HttpsUrl, HttpsUrl) -selectCustomBackend = - "select config_json_url, webapp_welcome_url from custom_backend where domain = ?" - -upsertCustomBackend :: PrepQuery W (HttpsUrl, HttpsUrl, Domain) () -upsertCustomBackend = - "update custom_backend set config_json_url = ?, webapp_welcome_url = ? where domain = ?" - -deleteCustomBackend :: PrepQuery W (Identity Domain) () -deleteCustomBackend = - "delete from custom_backend where domain = ?" diff --git a/services/galley/src/Galley/Cassandra/SearchVisibility.hs b/services/galley/src/Galley/Cassandra/SearchVisibility.hs deleted file mode 100644 index 9def57051e1..00000000000 --- a/services/galley/src/Galley/Cassandra/SearchVisibility.hs +++ /dev/null @@ -1,69 +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.SearchVisibility (interpretSearchVisibilityStoreToCassandra) where - -import Cassandra -import Data.Id -import Galley.Cassandra.Queries -import Galley.Cassandra.Store -import Galley.Cassandra.Util -import Galley.Effects.SearchVisibilityStore (SearchVisibilityStore (..)) -import Imports -import Polysemy -import Polysemy.Input -import Polysemy.TinyLog -import Wire.API.Team.SearchVisibility -import Wire.ConversationStore.Cassandra.Instances () - -interpretSearchVisibilityStoreToCassandra :: - ( Member (Embed IO) r, - Member (Input ClientState) r, - Member TinyLog r - ) => - Sem (SearchVisibilityStore ': r) a -> - Sem r a -interpretSearchVisibilityStoreToCassandra = interpret $ \case - GetSearchVisibility tid -> do - logEffect "SearchVisibilityStore.GetSearchVisibility" - embedClient $ getSearchVisibility tid - SetSearchVisibility tid value -> do - logEffect "SearchVisibilityStore.SetSearchVisibility" - embedClient $ setSearchVisibility tid value - ResetSearchVisibility tid -> do - logEffect "SearchVisibilityStore.ResetSearchVisibility" - embedClient $ resetSearchVisibility tid - --- | Return whether a given team is allowed to enable/disable sso -getSearchVisibility :: (MonadClient m) => TeamId -> m TeamSearchVisibility -getSearchVisibility tid = - toSearchVisibility <$> do - retry x1 $ query1 selectSearchVisibility (params LocalQuorum (Identity tid)) - where - -- The value is either set or we return the default - toSearchVisibility :: Maybe (Identity (Maybe TeamSearchVisibility)) -> TeamSearchVisibility - toSearchVisibility (Just (Identity (Just status))) = status - toSearchVisibility _ = SearchVisibilityStandard - --- | Determines whether a given team is allowed to enable/disable sso -setSearchVisibility :: (MonadClient m) => TeamId -> TeamSearchVisibility -> m () -setSearchVisibility tid visibilityType = do - retry x5 $ write updateSearchVisibility (params LocalQuorum (visibilityType, tid)) - -resetSearchVisibility :: (MonadClient m) => TeamId -> m () -resetSearchVisibility tid = do - retry x5 $ write updateSearchVisibility (params LocalQuorum (SearchVisibilityStandard, tid)) diff --git a/services/galley/src/Galley/Cassandra/Util.hs b/services/galley/src/Galley/Cassandra/Util.hs deleted file mode 100644 index f0cd114d5f4..00000000000 --- a/services/galley/src/Galley/Cassandra/Util.hs +++ /dev/null @@ -1,27 +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.Util where - -import Data.ByteString -import Imports -import Polysemy -import Polysemy.TinyLog -import System.Logger.Message - -logEffect :: (Member TinyLog r) => ByteString -> Sem r () -logEffect = debug . msg . val diff --git a/services/galley/src/Galley/Data/TeamNotifications.hs b/services/galley/src/Galley/Data/TeamNotifications.hs deleted file mode 100644 index d38af603110..00000000000 --- a/services/galley/src/Galley/Data/TeamNotifications.hs +++ /dev/null @@ -1,39 +0,0 @@ -{-# LANGUAGE StrictData #-} - --- 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 . - --- | See also: "Galley.API.TeamNotifications". --- --- This module is a clone of "Gundeck.Notification.Data". --- --- FUTUREWORK: this is a work-around because it only solves *some* problems with team events. --- We should really use a scalable message queue instead. -module Galley.Data.TeamNotifications (ResultPage (..)) where - -import Data.Sequence (Seq) -import Imports -import Wire.API.Internal.Notification - -data ResultPage = ResultPage - { -- | A sequence of notifications. - resultSeq :: Seq QueuedNotification, - -- | Whether there might be more notifications that can be - -- obtained through another query, starting the the ID of the - -- last notification in 'resultSeq'. - resultHasMore :: !Bool - } diff --git a/services/galley/src/Galley/Effects.hs b/services/galley/src/Galley/Effects.hs deleted file mode 100644 index c6bf5c7309f..00000000000 --- a/services/galley/src/Galley/Effects.hs +++ /dev/null @@ -1,192 +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.Effects - ( -- * Effects needed in Galley - GalleyEffects1, - - -- * Effects to access the Intra API - BrigAPIAccess, - FederationAPIAccess, - SparAPIAccess, - - -- * External services - ExternalAccess, - - -- * Fire-and-forget async - FireAndForget, - - -- * Store effects - UserClientIndexStore, - ConversationStore, - CustomBackendStore, - LegalHoldStore, - ProposalStore, - SearchVisibilityStore, - ServiceStore, - Random, - TeamFeatureStore, - TeamMemberStore, - TeamNotificationStore, - TeamStore, - - -- * Paging effects - ListItems, - - -- * Other effects - Queue, - - -- * Polysemy re-exports - Member, - Members, - - -- * Queueing effects - BackendNotificationQueueAccess, - ) -where - -import Data.Id -import Data.Map (Map) -import Data.Misc (HttpsUrl) -import Data.Qualified -import Data.Text (Text) -import Galley.Effects.CustomBackendStore -import Galley.Effects.Queue -import Galley.Effects.SearchVisibilityStore -import Galley.Effects.TeamMemberStore -import Galley.Effects.TeamNotificationStore -import Galley.Env -import Galley.Options -import Imports (Either) -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.API.Team.FeatureFlags -import Wire.BackendNotificationQueueAccess -import Wire.BrigAPIAccess -import Wire.CodeStore -import Wire.ConversationStore (ConversationStore, MLSCommitLockStore) -import Wire.ConversationSubsystem -import Wire.ExternalAccess -import Wire.FeaturesConfigSubsystem (FeaturesConfigSubsystem) -import Wire.FeaturesConfigSubsystem.Types (ExposeInvitationURLsAllowlist) -import Wire.FederationAPIAccess -import Wire.FederationSubsystem -import Wire.FireAndForget -import Wire.GundeckAPIAccess -import Wire.HashPassword -import Wire.LegalHoldStore -import Wire.LegalHoldStore.Env (LegalHoldEnv) -import Wire.ListItems -import Wire.MeetingsStore (MeetingsStore) -import Wire.MeetingsSubsystem (MeetingsSubsystem) -import Wire.MeetingsSubsystem.Interpreter qualified as Meeting -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.TeamFeatureStore -import Wire.TeamJournal (TeamJournal) -import Wire.TeamStore -import Wire.TeamSubsystem (TeamSubsystem) -import Wire.UserClientIndexStore -import Wire.UserGroupStore - --- All the possible high-level effects. -type GalleyEffects1 = - '[ MeetingsSubsystem, - ConversationSubsystem, - FederationSubsystem, - TeamCollaboratorsSubsystem, - Input AllTeamFeatures, - FeaturesConfigSubsystem, - TeamSubsystem, - SparAPIAccess, - NotificationSubsystem, - ExternalAccess, - BrigAPIAccess, - GundeckAPIAccess, - Rpc, - FederationAPIAccess FederatorClient, - BackendNotificationQueueAccess, - FireAndForget, - TeamCollaboratorsStore, - MeetingsStore, - UserClientIndexStore, - CodeStore, - ProposalStore, - RateLimit, - HashPassword, - Random, - CustomBackendStore, - SearchVisibilityStore, - TeamStore, - TeamJournal, - LegalHoldStore, - Input LegalHoldEnv, - UserGroupStore, - ServiceStore, - TeamNotificationStore, - ConversationStore, - MLSCommitLockStore, - TeamFeatureStore, - TeamMemberStore InternalPaging, - TeamMemberStore CassandraPaging, - ListItems LegacyPaging TeamId, - ListItems InternalPaging TeamId, - Input ExposeInvitationURLsAllowlist, - Input FeatureFlags, - Input FanoutLimit, - Input (FeatureDefaults LegalholdConfig), - Input (Local ()), - Input Opts, - Input (Either HttpsUrl (Map Text HttpsUrl)), - Now, - Queue DeleteItem, - Error Meeting.MeetingError, - Error DynError, - Error RateLimitExceeded, - ErrorS OperationDenied, - ErrorS 'HistoryNotSupported, - ErrorS 'NotATeamMember, - ErrorS 'ConvAccessDenied, - ErrorS 'NotConnected, - ErrorS 'MLSNotEnabled, - ErrorS 'MLSNonEmptyMemberList, - ErrorS 'MissingLegalholdConsent, - ErrorS 'NonBindingTeam, - ErrorS 'NoBindingTeamMembers, - ErrorS 'TeamNotFound, - ErrorS 'InvalidOperation, - ErrorS 'ConvNotFound, - ErrorS 'ChannelsNotEnabled, - ErrorS 'NotAnMlsConversation, - ErrorS 'NotATeamMember, - ErrorS 'MeetingNotFound, - ErrorS 'InvalidOperation - ] diff --git a/services/galley/src/Galley/Effects/SearchVisibilityStore.hs b/services/galley/src/Galley/Effects/SearchVisibilityStore.hs deleted file mode 100644 index cd7aecf21fb..00000000000 --- a/services/galley/src/Galley/Effects/SearchVisibilityStore.hs +++ /dev/null @@ -1,37 +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.SearchVisibilityStore - ( SearchVisibilityStore (..), - getSearchVisibility, - setSearchVisibility, - resetSearchVisibility, - ) -where - -import Data.Id -import Polysemy -import Wire.API.Team.SearchVisibility - -data SearchVisibilityStore m a where - GetSearchVisibility :: TeamId -> SearchVisibilityStore m TeamSearchVisibility - SetSearchVisibility :: TeamId -> TeamSearchVisibility -> SearchVisibilityStore m () - ResetSearchVisibility :: TeamId -> SearchVisibilityStore m () - -makeSem ''SearchVisibilityStore diff --git a/services/galley/src/Galley/Intra/Util.hs b/services/galley/src/Galley/Intra/Util.hs deleted file mode 100644 index 0f53e268c7f..00000000000 --- a/services/galley/src/Galley/Intra/Util.hs +++ /dev/null @@ -1,112 +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.Util - ( IntraComponent (..), - call, - callWithServant, - ) -where - -import Bilge hiding (getHeader, host, options, port, statusCode) -import Bilge qualified as B -import Bilge.RPC (rpc) -import Bilge.Retry -import Cassandra.Options (Endpoint (..)) -import Control.Lens (view) -import Control.Retry -import Data.ByteString.Lazy qualified as LB -import Data.Misc (portNumber) -import Data.Sequence (Seq (..)) -import Data.Text qualified as Text -import Data.Text.Encoding (encodeUtf8) -import Data.Text.Lazy qualified as LT -import Galley.Env hiding (brig) -import Galley.Monad -import Galley.Options -import Imports hiding (log) -import Network.HTTP.Types (statusIsServerError) -import Network.Wai.Utilities.Server -import Servant.Client qualified as Servant -import Servant.Client.Core qualified as Servant -import Wire.OpenTelemetry.Servant (otelClientMiddleware) - -data IntraComponent = Brig | Spar - deriving (Show) - -componentName :: IntraComponent -> String -componentName Brig = "brig" -componentName Spar = "spar" - -componentRequest :: IntraComponent -> Opts -> Request -> Request -componentRequest Brig o = - B.host (encodeUtf8 . host $ o._brig) - . B.port (portNumber $ fromIntegral . port $ o._brig) -componentRequest Spar o = - B.host (encodeUtf8 o._spar.host) - . B.port (portNumber $ fromIntegral . port $ o._spar) - -componentServantClient :: IntraComponent -> App Servant.ClientEnv -componentServantClient comp = do - mgr <- view manager - brigep <- case comp of - Brig -> view $ options . brig - Spar -> view $ options . spar - RequestId rId <- view reqId - let baseurl = Servant.BaseUrl Servant.Http (Text.unpack brigep.host) (fromIntegral brigep.port) "" - addRequestIdHeader app req = do - let reqWithId = req {Servant.requestHeaders = (defaultRequestIdHeaderName, rId) :<| req.requestHeaders} - app reqWithId - traceInfo = "intra-call-to-" <> Text.pack (componentName comp) - - pure $ - Servant.ClientEnv - { Servant.manager = mgr, - Servant.baseUrl = baseurl, - Servant.cookieJar = Nothing, - Servant.makeClientRequest = Servant.defaultMakeClientRequest, - Servant.middleware = - otelClientMiddleware traceInfo - . addRequestIdHeader - } - -componentRetryPolicy :: IntraComponent -> RetryPolicy -componentRetryPolicy Brig = x1 -componentRetryPolicy Spar = x1 - -call :: - IntraComponent -> - (Request -> Request) -> - App (Response (Maybe LB.ByteString)) -call comp r = do - o <- view options - let r0 = componentRequest comp o - let n = LT.pack (componentName comp) - recovering (componentRetryPolicy comp) rpcHandlers (const (rpc n (r . r0))) - -x1 :: RetryPolicy -x1 = limitRetries 1 - -callWithServant :: IntraComponent -> Servant.ClientM a -> App (Either Servant.ClientError a) -callWithServant comp action = do - env <- componentServantClient comp - let makeRequest = liftIO $ Servant.runClientM action env - shouldRetry (Right _) = False - shouldRetry (Left (Servant.FailureResponse _ resp)) = statusIsServerError resp.responseStatusCode - shouldRetry (Left (Servant.ConnectionError _)) = True - shouldRetry (Left _) = False - retrying (componentRetryPolicy comp) (const $ pure . shouldRetry) (const $ makeRequest) diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 6090f40f4a1..619a082d36e 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -2614,6 +2614,7 @@ mkProfile quid name = profileLegalholdStatus = defUserLegalHoldStatus, profileSupportedProtocols = defSupportedProtocols, profileType = UserTypeRegular, + profileApp = Nothing, profileSearchable = True } diff --git a/services/gen-aws-conf.sh b/services/gen-aws-conf.sh index 202bf473f7a..7b380e1341a 100755 --- a/services/gen-aws-conf.sh +++ b/services/gen-aws-conf.sh @@ -21,7 +21,7 @@ echo -e "\nHINT: when prompted for an integration config, consider trying 'z-con # Ensure that we have a file named integration-aws.yaml in the current # dir. If not, fetch it from a known location on S3 -if [ ! -f "${DIR}/integration-aws.yaml" ] +if [[ ! -f "${DIR}/integration-aws.yaml" ]] then echo "Could not find AWS config file to override settings, specify a location on S3 to download the file or add one at ${DIR}/integration-aws.yaml and retry: " read -r location @@ -32,7 +32,7 @@ services=( brig cargohold galley gundeck cannon proxy spar ) for service in "${services[@]}"; do yq r "${DIR}/integration-aws.yaml" "${service}" > "/tmp/${service}-aws.yaml" yq m -a "/tmp/${service}-aws.yaml" "${DIR}/${service}/${service}.integration.yaml" > "${DIR}/${service}/${service}.integration-aws.yaml" - if [ -e "${DIR}/${service}/${service}2.integration.yaml" ]; then + if [[ -e "${DIR}/${service}/${service}2.integration.yaml" ]]; then yq m -a "/tmp/${service}-aws.yaml" "${DIR}/${service}/${service}2.integration.yaml" > "${DIR}/${service}/${service}2.integration-aws.yaml" fi done diff --git a/services/gundeck/gundeck.integration.yaml b/services/gundeck/gundeck.integration.yaml index 1c33557402a..00c80574794 100644 --- a/services/gundeck/gundeck.integration.yaml +++ b/services/gundeck/gundeck.integration.yaml @@ -55,6 +55,7 @@ settings: # brig, cannon, cargohold, galley, gundeck, proxy, spar. disabledAPIVersions: [] cellsEventQueue: "cells_events" + consumableNotifications: false logLevel: Warn logNetStrings: false diff --git a/services/gundeck/src/Gundeck/API/Internal.hs b/services/gundeck/src/Gundeck/API/Internal.hs index c1c1591ab8d..296b8f4f87e 100644 --- a/services/gundeck/src/Gundeck/API/Internal.hs +++ b/services/gundeck/src/Gundeck/API/Internal.hs @@ -22,11 +22,12 @@ module Gundeck.API.Internal where import Cassandra qualified -import Control.Lens (view) +import Control.Lens (view, (^.)) import Data.Id import Gundeck.Client import Gundeck.Client qualified as Client import Gundeck.Monad +import Gundeck.Options (consumableNotifications, settings) import Gundeck.Presence qualified as Presence import Gundeck.Push qualified as Push import Gundeck.Push.Data qualified as PushTok @@ -69,6 +70,8 @@ getPushTokensH uid = PushTok.PushTokenList <$> (view PushTok.addrPushToken <$$> registerConsumableNotificationsClient :: UserId -> ClientId -> Gundeck NoContent registerConsumableNotificationsClient uid cid = do - chan <- getRabbitMqChan - void . liftIO $ setupConsumableNotifications chan uid cid + enabled <- asks (^. options . settings . consumableNotifications) + when enabled $ do + chan <- getRabbitMqChan + void . liftIO $ setupConsumableNotifications chan uid cid pure NoContent diff --git a/services/gundeck/src/Gundeck/Options.hs b/services/gundeck/src/Gundeck/Options.hs index ee55c98bebe..d70bbc4f91d 100644 --- a/services/gundeck/src/Gundeck/Options.hs +++ b/services/gundeck/src/Gundeck/Options.hs @@ -84,7 +84,9 @@ data Settings = Settings -- notifications from the database if notifications have inlined payloads. _internalPageSize :: Maybe Int32, -- | The name of the RabbitMQ queue to be used to forward events to Cells. - _cellsEventQueue :: !(Maybe Text) + _cellsEventQueue :: !(Maybe Text), + -- | Determines if consumable notifications are enabled + _consumableNotifications :: !Bool } deriving (Show, Generic) diff --git a/services/gundeck/src/Gundeck/Push.hs b/services/gundeck/src/Gundeck/Push.hs index 3609bb5dfb0..9a7e8d5295c 100644 --- a/services/gundeck/src/Gundeck/Push.hs +++ b/services/gundeck/src/Gundeck/Push.hs @@ -104,6 +104,7 @@ push ps = do -- | Abstract over all effects in 'pushAll' (for unit testing). class (MonadThrow m) => MonadPushAll m where + mpaConsumableNotificationsEnabled :: m Bool mpaNotificationTTL :: m NotificationTTL mpaCellsEventQueue :: m (Maybe Text) mpaMkNotificationId :: m NotificationId @@ -117,6 +118,7 @@ class (MonadThrow m) => MonadPushAll m where mpaPublishToRabbitMq :: Text -> Text -> Q.Message -> m () instance MonadPushAll Gundeck where + mpaConsumableNotificationsEnabled = view (options . settings . consumableNotifications) mpaNotificationTTL = view (options . settings . notificationTTL) mpaCellsEventQueue = view (options . settings . cellsEventQueue) mpaMkNotificationId = mkNotificationId @@ -241,13 +243,19 @@ getClients uids = do pushAll :: (MonadPushAll m, MonadNativeTargets m, MonadMapAsync m, Log.MonadLogger m) => [Push] -> m () pushAll pushes = do Log.debug $ msg (val "pushing") . Log.field "pushes" (Aeson.encode pushes) - (rabbitmqPushes, legacyPushes, allUserClients) <- splitPushes pushes + consumableNotificationsEnabled <- mpaConsumableNotificationsEnabled + (rabbitmqPushes, legacyPushes, allUserClients) <- + if consumableNotificationsEnabled + then splitPushes pushes + else do + allUserClients <- mpaGetClients (Set.unions $ map (\p -> Set.map (._recipientId) $ p._pushRecipients) pushes) + pure ([], pushes, allUserClients) legacyNotifs <- mapM mkNewNotification legacyPushes pushAllLegacy legacyNotifs allUserClients rabbitmqNotifs <- mapM mkNewNotification rabbitmqPushes - pushAllViaRabbitMq rabbitmqNotifs allUserClients + unless (null rabbitmqNotifs) $ pushAllViaRabbitMq rabbitmqNotifs allUserClients -- Note that Cells needs all notifications because it doesn't matter whether -- some recipients have rabbitmq clients or not. diff --git a/services/gundeck/test/unit/MockGundeck.hs b/services/gundeck/test/unit/MockGundeck.hs index cb7b4f5fa88..f6d6ebe44f0 100644 --- a/services/gundeck/test/unit/MockGundeck.hs +++ b/services/gundeck/test/unit/MockGundeck.hs @@ -433,6 +433,7 @@ instance MonadThrow MockGundeck where -- as well crash badly here, as long as it doesn't go unnoticed...) instance MonadPushAll MockGundeck where + mpaConsumableNotificationsEnabled = pure True mpaNotificationTTL = pure $ NotificationTTL 300 -- (longer than we want any test to take.) mpaCellsEventQueue = pure $ Just "cells" mpaMkNotificationId = mockMkNotificationId diff --git a/services/nginz/integration-test/conf/nginz/nginx.conf b/services/nginz/integration-test/conf/nginz/nginx.conf index 4b9356f07c5..21ab0c71a4e 100644 --- a/services/nginz/integration-test/conf/nginz/nginx.conf +++ b/services/nginz/integration-test/conf/nginz/nginx.conf @@ -62,7 +62,7 @@ http { # However we do not want to log access tokens. # - log_format custom_zeta '$remote_addr - $remote_user [$time_local] "$sanitized_request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$http_x_forwarded_for" - $connection $request_time $upstream_response_time $upstream_cache_status $zauth_user $zauth_connection $request_id $proxy_protocol_addr'; + log_format custom_zeta '$remote_addr - $remote_user [$time_local] "$sanitized_request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$http_x_forwarded_for" - $connection $request_time $upstream_response_time $upstream_cache_status $zauth_user $zauth_connection $request_id $proxy_protocol_addr "$http_wire_client" "$http_wire_client_version" "$http_wire_config_hash"'; access_log /dev/stdout custom_zeta; # diff --git a/services/spar/src/Spar/API.hs b/services/spar/src/Spar/API.hs index 5cc0cc6ac45..c5e62375b9f 100644 --- a/services/spar/src/Spar/API.hs +++ b/services/spar/src/Spar/API.hs @@ -28,7 +28,7 @@ -- SCIM branch of the API is fully defined in "Spar.Scim". module Spar.API ( -- * Server - app, + Spar.API.app, api, -- * API types diff --git a/services/spar/src/Spar/Intra/Galley.hs b/services/spar/src/Spar/Intra/Galley.hs index b938931c56a..31e3e89ba88 100644 --- a/services/spar/src/Spar/Intra/Galley.hs +++ b/services/spar/src/Spar/Intra/Galley.hs @@ -108,7 +108,7 @@ isEmailValidationEnabledTeam tid = do resp <- call $ method GET . paths ["i", "teams", toByteString' tid, "features", "validateSAMLemails"] pure ( statusCode resp == 200 - && ( ((.status) <$> responseJsonMaybe @(LockableFeature ValidateSAMLEmailsConfig) resp) + && ( ((.status) <$> responseJsonMaybe @(LockableFeature RequireExternalEmailVerificationConfig) resp) == Just FeatureStatusEnabled ) ) diff --git a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs index 48a631f3a27..fa651962ee1 100644 --- a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs +++ b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs @@ -581,8 +581,8 @@ specCreateUser = describe "POST /Users" $ do it "adds a Wire scheme to the user record" $ testSchemaIsAdded it "set locale to default and update to de" $ testCreateUserWithSamlIdPWithPreferredLanguage Nothing (Just (Locale (Language DE) Nothing)) it "set locale to fr and update to de" $ testCreateUserWithSamlIdPWithPreferredLanguage (Just (Locale (Language FR) Nothing)) (Just (Locale (Language DE) Nothing)) - it "set locale to hr and update to de" $ testCreateUserWithSamlIdPWithPreferredLanguage (Just (Locale (Language HR) Nothing)) (Just (Locale (Language DE) Nothing)) - it "set locale to hr and update to default" $ testCreateUserWithSamlIdPWithPreferredLanguage (Just (Locale (Language HR) Nothing)) Nothing + it "set locale to hr and update to de" $ testCreateUserWithSamlIdPWithPreferredLanguage (Just (Locale (Language Data.LanguageCodes.HR) Nothing)) (Just (Locale (Language DE) Nothing)) + it "set locale to hr and update to default" $ testCreateUserWithSamlIdPWithPreferredLanguage (Just (Locale (Language Data.LanguageCodes.HR) Nothing)) Nothing it "set locale to default and update to default" $ testCreateUserWithSamlIdPWithPreferredLanguage Nothing Nothing it "requires externalId to be present" $ testExternalIdIsRequired it "testCreateRejectsInvalidHandle - rejects invalid handle" $ testCreateRejectsInvalidHandle diff --git a/services/spar/test-integration/Util/Email.hs b/services/spar/test-integration/Util/Email.hs index 0809638ef2a..b9bf4761eed 100644 --- a/services/spar/test-integration/Util/Email.hs +++ b/services/spar/test-integration/Util/Email.hs @@ -102,6 +102,6 @@ activate brig (k, c) = setSamlEmailValidation :: (HasCallStack) => TeamId -> Feature.FeatureStatus -> TestSpar () setSamlEmailValidation tid status = do galley <- view teGalley - let req = put $ galley . paths p . json (Feature.Feature @Feature.ValidateSAMLEmailsConfig status Feature.ValidateSAMLEmailsConfig) - p = ["/i/teams", toByteString' tid, "features", Feature.featureNameBS @Feature.ValidateSAMLEmailsConfig] + let req = put $ galley . paths p . json (Feature.Feature @Feature.RequireExternalEmailVerificationConfig status Feature.RequireExternalEmailVerificationConfig) + p = ["/i/teams", toByteString' tid, "features", Feature.featureNameBS @Feature.RequireExternalEmailVerificationConfig] call req !!! const 200 === statusCode diff --git a/services/spar/test-scim-suite/run.sh b/services/spar/test-scim-suite/run.sh index af1febc3668..8f902a10f01 100755 --- a/services/spar/test-scim-suite/run.sh +++ b/services/spar/test-scim-suite/run.sh @@ -39,13 +39,14 @@ function create_team_and_scim_token { export SCIM_TOKEN SCIM_TOKEN_ID=$(echo "$SCIM_TOKEN_FULL" | jq -r .info.id) export SCIM_TOKEN_ID - echo "$SCIM_TOKEN" + return 0 } -function create_env_file { + function create_env_file { + local token token=$(create_team_and_scim_token) - cat > /tmp/scim_test_suite_env.json < /tmp/scim_test_suite_env.json <&2 + local message="$*" + if [[ "$VERBOSE" -eq 1 ]]; then + echo -e "${GREEN}[INFO]${NC} $message" >&2 fi + return 0 } log_error() { - echo -e "${RED}[ERROR]${NC} $*" >&2 + local message="$*" + echo -e "${RED}[ERROR]${NC} $message" >&2 + return 0 } log_warn() { - echo -e "${YELLOW}[WARN]${NC} $*" >&2 + local message="$*" + echo -e "${YELLOW}[WARN]${NC} $message" >&2 + return 0 } # Generate random string random_string() { - local length=${1:-8} + local length="${1:-8}" env LC_CTYPE=C tr -dc a-zA-Z0-9 < /dev/urandom | head -c "$length" + return 0 } # Create a team admin user via Brig internal API @@ -78,7 +85,7 @@ create_team_admin() { local body body=$(echo "$response" | head -n-1) - if [ "$http_code" != "201" ]; then + if [[ "$http_code" != "201" ]]; then log_error "Failed to create team admin (HTTP $http_code)" echo "$body" >&2 return 1 @@ -89,7 +96,7 @@ create_team_admin() { local team_id team_id=$(echo "$body" | jq -r '.team') - if [ -z "$user_id" ] || [ "$user_id" = "null" ]; then + if [[ -z "$user_id" ]] || [[ "$user_id" = "null" ]]; then log_error "Failed to extract user ID from response" return 1 fi @@ -120,7 +127,7 @@ login() { local body body=$(echo "$response" | head -n-1) - if [ "$http_code" != "200" ]; then + if [[ "$http_code" != "200" ]]; then log_error "Login failed (HTTP $http_code)" echo "$body" >&2 return 1 @@ -129,13 +136,14 @@ login() { local access_token access_token=$(echo "$body" | jq -r '.access_token') - if [ -z "$access_token" ] || [ "$access_token" = "null" ]; then + if [[ -z "$access_token" ]] || [[ "$access_token" = "null" ]]; then log_error "Failed to extract access token from response" return 1 fi log "Logged in successfully" echo "$access_token" + return 0 } # Enable channels feature for a team @@ -150,7 +158,7 @@ enable_channels() { local unlock_code unlock_code=$(echo "$unlock_response" | tail -n1) - if [ "$unlock_code" != "200" ]; then + if [[ "$unlock_code" != "200" ]]; then log_warn "Failed to unlock channels feature (HTTP $unlock_code), continuing anyway..." fi @@ -167,7 +175,7 @@ enable_channels() { local body body=$(echo "$response" | head -n-1) - if [ "$http_code" != "200" ]; then + if [[ "$http_code" != "200" ]]; then log_error "Failed to enable channels feature (HTTP $http_code)" echo "$body" >&2 return 1 @@ -180,6 +188,7 @@ enable_channels() { local verify_response verify_response=$(curl -s -X GET "$GALLEY_INTERNAL/i/teams/$team_id/features/channels") log "Feature status: $verify_response" + return 0 } # Create a user group @@ -206,7 +215,7 @@ create_user_group() { local body body=$(echo "$response" | head -n-1) - if [ "$http_code" != "200" ] && [ "$http_code" != "201" ]; then + if [[ "$http_code" != "200" ]] && [[ "$http_code" != "201" ]]; then log_error "Failed to create user group (HTTP $http_code)" echo "$body" >&2 return 1 @@ -215,13 +224,14 @@ create_user_group() { local group_id group_id=$(echo "$body" | jq -r '.id') - if [ -z "$group_id" ] || [ "$group_id" = "null" ]; then + if [[ -z "$group_id" ]] || [[ "$group_id" = "null" ]]; then log_error "Failed to extract group ID from response" return 1 fi log "Created user group: $group_id" echo "$group_id" + return 0 } # Main test flow @@ -313,7 +323,7 @@ main() { EOF log "Input file: $input_file" - if [ "$VERBOSE" -eq 1 ]; then + if [[ "$VERBOSE" -eq 1 ]]; then log "Contents:" cat "$input_file" >&2 fi @@ -328,7 +338,7 @@ EOF log "Running CLI tool via cabal" local cli_opts=() - if [ "$VERBOSE" -eq 1 ]; then + if [[ "$VERBOSE" -eq 1 ]]; then cli_opts+=("-v") fi @@ -349,7 +359,7 @@ EOF local cli_exit_code=$? - if [ $cli_exit_code -ne 0 ]; then + if [[ $cli_exit_code -ne 0 ]]; then log_error "CLI tool failed with exit code $cli_exit_code" log_error "Output:" cat "$output_file" >&2 @@ -379,7 +389,7 @@ EOF local result_count result_count=$(echo "$result_groups" | wc -l) - if [ "$result_count" -ne 3 ]; then + if [[ "$result_count" -ne 3 ]]; then log_error "Expected 3 groups in results, got $result_count" rm -f "$input_file" "$output_file" exit 1 @@ -402,7 +412,7 @@ EOF log "Failed channels: $failed_channels" log "Successful associations: $successful_associations" - if [ "$successful_channels" -eq 0 ]; then + if [[ "$successful_channels" -eq 0 ]]; then log_error "No channels were created successfully" log "Full output:" jq '.' "$output_file" >&2 @@ -410,7 +420,7 @@ EOF exit 1 fi - if [ "$successful_associations" -eq 0 ]; then + if [[ "$successful_associations" -eq 0 ]]; then log_error "No associations were successful" log "Full output:" jq '.' "$output_file" >&2 @@ -429,7 +439,7 @@ EOF log "Total Channels Created: $successful_channels/$total_channels" log "Successful Associations: $successful_associations/3" - if [ "$VERBOSE" -eq 1 ]; then + if [[ "$VERBOSE" -eq 1 ]]; then log "" log "=== Full Results ===" jq '.' "$output_file" >&2 @@ -440,6 +450,7 @@ EOF # Cleanup rm -f "$input_file" "$output_file" + return 0 } # Run main diff --git a/tools/hlint.sh b/tools/hlint.sh index 0ac67b25f24..61e391792b3 100755 --- a/tools/hlint.sh +++ b/tools/hlint.sh @@ -11,20 +11,20 @@ while getopts ':f:m:k' opt do case $opt in f) f=${OPTARG} - if [ "$f" = "all" ]; then + if [[ "$f" = "all" ]]; then files=$(git ls-files | grep \.hs\$) - elif [ "$f" = "pr" ]; then + elif [[ "$f" = "pr" ]]; then files=$(git diff --name-only "$PR_BASE"... | grep \.hs\$) - elif [ "$f" = "changeset" ]; then + elif [[ "$f" = "changeset" ]]; then files=$(git diff --name-only HEAD | grep \.hs\$) else usage fi ;; m) m=${OPTARG} - if [ "$m" = "inplace" ]; then + if [[ "$m" = "inplace" ]]; then : - elif [ "$m" = "check" ]; then + elif [[ "$m" = "check" ]]; then : else usage @@ -35,16 +35,16 @@ while getopts ':f:m:k' opt esac done -if [ -z "${f}" ] || [ -z "${m}" ]; then +if [[ -z "${f}" ]] || [[ -z "${m}" ]]; then usage fi -if [ "${k}" ]; then - echo "Will fail on the first error" +if [[ "${k}" ]]; then + echo "Will fail on the first error" >&2 set -euo pipefail fi -if [ "$f" = "all" ] && [ "$m" = "check" ]; then +if [[ "$f" = "all" ]] && [[ "$m" = "check" ]]; then hlint -g else count=$(echo "$files" | grep -c -v -e '^[[:space:]]*$') @@ -52,8 +52,8 @@ else for f in $files do echo "$f" - if [ -e "$f" ]; then - if [ "$m" = "check" ]; then + if [[ -e "$f" ]]; then + if [[ "$m" = "check" ]]; then hlint --no-summary "$f" else hlint --refactor --refactor-options="--inplace" "$f" || \ diff --git a/tools/mlsstats/src/MlsStats/Run.hs b/tools/mlsstats/src/MlsStats/Run.hs index 189c600a5e1..3523b8252a9 100644 --- a/tools/mlsstats/src/MlsStats/Run.hs +++ b/tools/mlsstats/src/MlsStats/Run.hs @@ -69,7 +69,7 @@ run o = do $ Log.defSettings initCas casHost casPort casKeyspace l = C.init - . C.setLogger (C.mkLogger l) + . C.setLogger (C.mkLogger Nothing l) . C.setContacts casHost [] . C.setPortNumber (fromIntegral casPort) . C.setProtocolVersion C.V4 diff --git a/tools/ormolu1.sh b/tools/ormolu1.sh index 1210c58672a..05b963c2ac3 100755 --- a/tools/ormolu1.sh +++ b/tools/ormolu1.sh @@ -36,9 +36,9 @@ while getopts ":f:ch" opt; do case ${opt} in f) f=${OPTARG} - if [ "$f" = "pr" ]; then + if [[ "$f" = "pr" ]]; then ALLOW_DIRTY_WC=1 - elif [ "$f" = "all" ]; then + elif [[ "$f" = "all" ]]; then ALLOW_DIRTY_WC=1 else usage @@ -56,13 +56,13 @@ while getopts ":f:ch" opt; do done shift $((OPTIND - 1)) -if [ "$#" -ne 0 ]; then +if [[ "$#" -ne 0 ]]; then echo "$USAGE" 1>&2 exit 1 fi -if [ "$(git status -s | grep -v \?\?)" != "" ]; then - if [ "$ALLOW_DIRTY_WC" == "1" ]; then +if [[ "$(git status -s | grep -v \?\?)" != "" ]]; then + if [[ "$ALLOW_DIRTY_WC" == "1" ]]; then : else echo "Working copy is not clean." @@ -76,13 +76,13 @@ echo "language extensions are taken from the resp. cabal files" FAILURES=0 -if [ -t 1 ]; then +if [[ -t 1 ]]; then : "${ORMOLU_CONDENSE_OUTPUT:=1}" fi -if [ "$f" = "all" ] || [ "$f" = "" ]; then +if [[ "$f" = "all" ]] || [[ "$f" = "" ]]; then files=$(git ls-files | grep '\.hsc\?$') -elif [ "$f" = "pr" ]; then +elif [[ "$f" = "pr" ]]; then files=$( git diff --diff-filter=ACMR --name-only "$PR_BASE"... | { grep '\.hsc\?$' || true; } git diff --diff-filter=ACMR --name-only HEAD | { grep \.hs\$ || true; } @@ -97,21 +97,21 @@ for hsfile in $files; do ormolu --mode $ARG_ORMOLU_MODE --check-idempotence "$hsfile" & wait $! && err=0 || err=$? - if [ "$err" == "100" ]; then + if [[ "$err" == "100" ]]; then ((++FAILURES)) echo "$hsfile... *** FAILED" clear="" - elif [ "$err" == "0" ]; then + elif [[ "$err" == "0" ]]; then echo -e "$clear$hsfile... ok" - [ "$ORMOLU_CONDENSE_OUTPUT" == "1" ] && clear="\033[A\r\033[K" + [[ "$ORMOLU_CONDENSE_OUTPUT" == "1" ]] && clear="\033[A\r\033[K" else exit "$err" fi done -if [ "$FAILURES" != 0 ]; then +if [[ "$FAILURES" != 0 ]]; then echo "ormolu failed on $FAILURES files." - if [ "$ARG_ORMOLU_MODE" == "check" ]; then + if [[ "$ARG_ORMOLU_MODE" == "check" ]]; then echo -en "\n\nyou can fix this by running 'make format' from the git repo root.\n\n" fi exit 1 diff --git a/tools/rebase-onto-formatter.sh b/tools/rebase-onto-formatter.sh index 3672f6a1f25..5e9b8610214 100755 --- a/tools/rebase-onto-formatter.sh +++ b/tools/rebase-onto-formatter.sh @@ -36,7 +36,7 @@ INSTRUCTIONS: " -if [ -z "$BASE_COMMIT" ] || [ -z "$TARGET_COMMIT" ] || [ -z "$FORMATTING_COMMAND" ] +if [[ -z "$BASE_COMMIT" ]] || [[ -z "$TARGET_COMMIT" ]] || [[ -z "$FORMATTING_COMMAND" ]] then echo "$USAGE" 1>&2 exit 1 diff --git a/tools/sftd_disco/sftd_disco.sh b/tools/sftd_disco/sftd_disco.sh index 28e0f5a7ca2..2cf660cdb3a 100755 --- a/tools/sftd_disco/sftd_disco.sh +++ b/tools/sftd_disco/sftd_disco.sh @@ -27,8 +27,8 @@ function valid_url() { # 4. save the resulting URLs as a json array to a file # this file can then be served from nginx running besides sft function upstream() { - name=$1 - entries=$(dig +short +retries=3 +search SRV "${name}" | sort) + local srv_name="$1" + entries=$(dig +short +retries=3 +search SRV "${srv_name}" | sort) unset servers comma="" IFS=$'\t\n' @@ -43,7 +43,7 @@ function upstream() { fi done # shellcheck disable=SC2128 - if [ -n "$servers" ]; then + if [[ -n "$servers" ]]; then echo '{"sft_servers_all": ['"${servers[*]}"']}' | jq >${new} else printf "" >>${new} @@ -51,7 +51,7 @@ function upstream() { } function routing_disco() { - srv_name=$1 + local srv_name="$1" ivl=$(echo | awk '{ srand(); printf("%f", 2.5 + rand() * 1.5) }') [[ -f $old ]] || touch -d "1970-01-01" $old @@ -68,6 +68,7 @@ function routing_disco() { echo done, sleeping "$ivl" sleep "$ivl" + return 0 } while true; do diff --git a/tools/stern/src/Stern/API.hs b/tools/stern/src/Stern/API.hs index 0ba12df2135..dd1b104d67a 100644 --- a/tools/stern/src/Stern/API.hs +++ b/tools/stern/src/Stern/API.hs @@ -36,6 +36,7 @@ import Data.Aeson.KeyMap qualified as KeyMap import Data.Aeson.Types (emptyArray) import Data.ByteString (fromStrict) import Data.ByteString.Conversion +import Data.CommaSeparatedList (CommaSeparatedList (..)) import Data.Handle (Handle) import Data.Id import Data.Proxy (Proxy (..)) @@ -154,8 +155,8 @@ sitemap' = :<|> Named @"put-route-sso-config" (mkFeatureStatusPutRoute @SSOConfig) :<|> Named @"get-route-search-visibility-available-config" (mkFeatureGetRoute @SearchVisibilityAvailableConfig) :<|> Named @"put-route-search-visibility-available-config" (mkFeatureStatusPutRoute @SearchVisibilityAvailableConfig) - :<|> Named @"get-route-validate-saml-emails-config" (mkFeatureGetRoute @ValidateSAMLEmailsConfig) - :<|> Named @"put-route-validate-saml-emails-config" (mkFeatureStatusPutRoute @ValidateSAMLEmailsConfig) + :<|> Named @"get-route-validate-saml-emails-config" (mkFeatureGetRoute @RequireExternalEmailVerificationConfig) + :<|> Named @"put-route-validate-saml-emails-config" (mkFeatureStatusPutRoute @RequireExternalEmailVerificationConfig) :<|> Named @"get-route-digital-signatures-config" (mkFeatureGetRoute @DigitalSignaturesConfig) :<|> Named @"put-route-digital-signatures-config" (mkFeatureStatusPutRoute @DigitalSignaturesConfig) :<|> Named @"get-route-file-sharing-config" (mkFeatureGetRoute @FileSharingConfig) @@ -256,20 +257,20 @@ unsuspendUser uid = NoContent <$ Intra.putUserStatus Active uid usersByEmail :: EmailAddress -> Handler [User] usersByEmail = Intra.getUserProfilesByIdentity -usersByIds :: [UserId] -> Handler [User] -usersByIds = Intra.getUserProfiles . Left +usersByIds :: CommaSeparatedList UserId -> Handler [User] +usersByIds = Intra.getUserProfiles . Left . fromCommaSeparatedList -usersByHandles :: [Handle] -> Handler [User] -usersByHandles = Intra.getUserProfiles . Right +usersByHandles :: CommaSeparatedList Handle -> Handler [User] +usersByHandles = Intra.getUserProfiles . Right . fromCommaSeparatedList -ejpdInfoByHandles :: Maybe Bool -> [Handle] -> Handler EJPD.EJPDResponseBody -ejpdInfoByHandles (fromMaybe False -> includeContacts) handles = Intra.getEjpdInfo handles includeContacts +ejpdInfoByHandles :: Maybe Bool -> CommaSeparatedList Handle -> Handler EJPD.EJPDResponseBody +ejpdInfoByHandles (fromMaybe False -> includeContacts) = (`Intra.getEjpdInfo` includeContacts) . fromCommaSeparatedList userConnections :: UserId -> Handler UserConnectionGroups userConnections = fmap groupByStatus . Intra.getUserConnections -usersConnections :: [UserId] -> Handler [ConnectionStatus] -usersConnections = Intra.getUsersConnections . List +usersConnections :: CommaSeparatedList UserId -> Handler [ConnectionStatus] +usersConnections = Intra.getUsersConnections . List . fromCommaSeparatedList searchOnBehalf :: UserId -> Maybe T.Text -> Maybe Int32 -> Handler (SearchResult Contact) searchOnBehalf diff --git a/tools/stern/src/Stern/API/Routes.hs b/tools/stern/src/Stern/API/Routes.hs index e50562ee9dc..d3152fe4158 100644 --- a/tools/stern/src/Stern/API/Routes.hs +++ b/tools/stern/src/Stern/API/Routes.hs @@ -27,6 +27,7 @@ where import Control.Lens import Data.Aeson qualified as A +import Data.CommaSeparatedList (CommaSeparatedList) import Data.Handle import Data.Id import Data.Kind @@ -93,7 +94,7 @@ type SternAPI = ( Summary "Displays active users info given a list of ids" :> "users" :> "by-ids" - :> QueryParam' [Required, Strict, Description "List of IDs of the users, separated by comma"] "ids" [UserId] + :> QueryParam' [Required, Strict, Description "List of IDs of the users, separated by comma"] "ids" (CommaSeparatedList UserId) :> Get '[JSON] [User] ) :<|> Named @@ -101,7 +102,7 @@ type SternAPI = ( Summary "Displays active users info given a list of handles" :> "users" :> "by-handles" - :> QueryParam' [Required, Strict, Description "List of Handles of the users, without '@', separated by comma"] "handles" [Handle] + :> QueryParam' [Required, Strict, Description "List of Handles of the users, without '@', separated by comma"] "handles" (CommaSeparatedList Handle) :> Get '[JSON] [User] ) :<|> Named @@ -118,7 +119,7 @@ type SternAPI = ( Summary "Displays connections of many users given a list of ids" :> "users" :> "connections" - :> QueryParam' [Required, Strict, Description "List of IDs of the users, separated by comma"] "ids" [UserId] + :> QueryParam' [Required, Strict, Description "List of IDs of the users, separated by comma"] "ids" (CommaSeparatedList UserId) :> Get '[JSON] [ConnectionStatus] ) :<|> Named @@ -200,7 +201,7 @@ type SternAPI = ( Summary "internal wire.com process: https://wearezeta.atlassian.net/wiki/spaces/~463749889/pages/256738296/EJPD+official+requests+process" :> "ejpd-info" :> QueryParam' [Optional, Strict, Description "If 'true', this gives you more more exhaustive information about this user (including social network)"] "include_contacts" Bool - :> QueryParam' [Required, Strict, Description "Handles of the users, separated by commas (NB: all chars need to be lower case!)"] "handles" [Handle] + :> QueryParam' [Required, Strict, Description "Handles of the users, separated by commas (NB: all chars need to be lower case!)"] "handles" (CommaSeparatedList Handle) :> Get '[JSON] EJPD.EJPDResponseBody ) :<|> Named @@ -255,8 +256,8 @@ type SternAPI = :<|> Named "put-route-sso-config" (MkFeatureStatusPutRoute SSOConfig) :<|> Named "get-route-search-visibility-available-config" (MkFeatureGetRoute SearchVisibilityAvailableConfig) :<|> Named "put-route-search-visibility-available-config" (MkFeatureStatusPutRoute SearchVisibilityAvailableConfig) - :<|> Named "get-route-validate-saml-emails-config" (MkFeatureGetRoute ValidateSAMLEmailsConfig) - :<|> Named "put-route-validate-saml-emails-config" (MkFeatureStatusPutRoute ValidateSAMLEmailsConfig) + :<|> Named "get-route-validate-saml-emails-config" (MkFeatureGetRoute RequireExternalEmailVerificationConfig) + :<|> Named "put-route-validate-saml-emails-config" (MkFeatureStatusPutRoute RequireExternalEmailVerificationConfig) :<|> Named "get-route-digital-signatures-config" (MkFeatureGetRoute DigitalSignaturesConfig) :<|> Named "put-route-digital-signatures-config" (MkFeatureStatusPutRoute DigitalSignaturesConfig) :<|> Named "get-route-file-sharing-config" (MkFeatureGetRoute FileSharingConfig) diff --git a/tools/stern/test/integration/API.hs b/tools/stern/test/integration/API.hs index 7d80f83d9f4..942e2e6bc2a 100644 --- a/tools/stern/test/integration/API.hs +++ b/tools/stern/test/integration/API.hs @@ -87,7 +87,7 @@ tests s = test s "GET /teams/:tid/admins" testGetTeamAdminInfo, test s "/teams/:tid/features/legalhold" testLegalholdConfig, test s "/teams/:tid/features/sso" $ testFeatureStatus @SSOConfig, - test s "/teams/:tid/features/validateSamlEmails" $ testFeatureStatus @ValidateSAMLEmailsConfig, + test s "/teams/:tid/features/validateSamlEmails" $ testFeatureStatus @RequireExternalEmailVerificationConfig, test s "/teams/:tid/features/digitalSignatures" $ testFeatureStatus @DigitalSignaturesConfig, test s "/teams/:tid/features/fileSharing" $ testFeatureStatus @FileSharingConfig, test s "/teams/:tid/features/conference-calling" $ testFeatureStatusOptTtl defConfCalling (Just FeatureTTLUnlimited), diff --git a/treefmt.toml b/treefmt.toml index fd38436758e..1cd7408a8ff 100644 --- a/treefmt.toml +++ b/treefmt.toml @@ -12,6 +12,7 @@ excludes = [ [formatter.shellcheck] command = "shellcheck" +options = ["-x"] includes = ["*.sh"] excludes = [ "dist-newstyle/",